diff --git a/go/apps/api/routes/v2_apis_create_api/401_test.go b/go/apps/api/routes/v2_apis_create_api/401_test.go index e0c87fe079..fe481d56a9 100644 --- a/go/apps/api/routes/v2_apis_create_api/401_test.go +++ b/go/apps/api/routes/v2_apis_create_api/401_test.go @@ -1,12 +1,12 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_create_api" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) // TestCreateApi_Unauthorized verifies that API creation requests are properly @@ -14,32 +14,19 @@ import ( // authorization tokens result in 401 Unauthorized responses, preventing // unauthorized access to the API creation endpoint. func TestCreateApi_Unauthorized(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // This test validates that requests with malformed or invalid authorization - // tokens are rejected with a 401 status code, ensuring proper security - // boundaries for the API creation endpoint. - t.Run("invalid auth token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ - Name: "test-api", - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status, "expected 401, sent: %+v, received: %s", req, res.RawBody) - }) - + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Name: "test-api", + } + }, + ) } diff --git a/go/apps/api/routes/v2_apis_create_api/403_test.go b/go/apps/api/routes/v2_apis_create_api/403_test.go index 8602f2c3f7..961fa9dff0 100644 --- a/go/apps/api/routes/v2_apis_create_api/403_test.go +++ b/go/apps/api/routes/v2_apis_create_api/403_test.go @@ -1,15 +1,12 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_create_api" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) // TestCreateApi_Forbidden verifies that API creation requests are properly @@ -17,80 +14,22 @@ import ( // ensures that RBAC (Role-Based Access Control) is correctly enforced and that // users without api.*.create_api permission receive 403 Forbidden responses. func TestCreateApi_Forbidden(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a root key with insufficient permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.create_identity") // Not api.*.create_api - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - // This test validates that a root key with valid authentication but - // insufficient permissions (lacking api.*.create_api) is properly rejected - // with a 403 status code, ensuring permission boundaries are enforced. - t.Run("insufficient permissions", func(t *testing.T) { - req := handler.Request{ - Name: "test-api", - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status) - }) - - // This test validates various permission combinations to ensure that only - // root keys with the exact api.*.create_api permission can create APIs, while - // keys with other permissions or insufficient permissions are rejected. - t.Run("permission combinations", func(t *testing.T) { - testCases := []struct { - name string - permissions []string - shouldPass bool - }{ - {name: "specific permission", permissions: []string{"api.*.create_api"}, shouldPass: true}, - {name: "specific permission and more", permissions: []string{"some.other.permission", "xxx", "api.*.create_api", "another.permission"}, shouldPass: true}, - {name: "insufficient permission", permissions: []string{"api.*.read_api"}, shouldPass: false}, - {name: "unrelated permission", permissions: []string{"identity.*.create_identity"}, shouldPass: false}, - } - - // Each test case validates a specific permission scenario to ensure - // proper RBAC enforcement across different permission combinations. - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create a root key with the specific permissions - permRootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, tc.permissions...) - permHeaders := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", permRootKey)}, - } - - req := handler.Request{ - Name: "test-api-permissions", + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Auditlogs: h.Auditlogs, } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, permHeaders, req) - - if tc.shouldPass { - require.Equal(t, 200, res.Status, "Expected 200 for permission: %v, got: %s", tc.permissions, res.RawBody) - require.NotEmpty(t, res.Body.Data.ApiId) - - // Verify the API in the database - api, err := db.Query.FindApiByID(context.Background(), h.DB.RO(), res.Body.Data.ApiId) - require.NoError(t, err) - require.Equal(t, req.Name, api.Name) - } else { - require.Equal(t, http.StatusForbidden, res.Status, "Expected 403 for permission: %v, got: %s", tc.permissions, res.RawBody) + }, + RequiredPermissions: []string{"api.*.create_api"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Name: "test-api", } - }) - } - }) + }, + }, + ) } diff --git a/go/apps/api/routes/v2_apis_delete_api/401_test.go b/go/apps/api/routes/v2_apis_delete_api/401_test.go index 12edbd57ce..8443ade200 100644 --- a/go/apps/api/routes/v2_apis_delete_api/401_test.go +++ b/go/apps/api/routes/v2_apis_delete_api/401_test.go @@ -1,90 +1,30 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_delete_api" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - Caches: h.Caches, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - ApiId: "api_1234", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Equal(t, "Authorization header for 'bearer' scheme", res.Body.Error.Detail) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Equal(t, "The provided root key is invalid. We could not find the requested key.", res.Body.Error.Detail) - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Equal(t, "You must provide a valid root key in the Authorization header in the format 'Bearer ROOT_KEY'. Your authorization header is missing the 'Bearer ' prefix.", res.Body.Error.Detail) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Auditlogs: h.Auditlogs, + Caches: h.Caches, + } + }, + func() handler.Request { + return handler.Request{ + ApiId: uid.New(uid.APIPrefix), + } + }, + ) } diff --git a/go/apps/api/routes/v2_apis_delete_api/403_test.go b/go/apps/api/routes/v2_apis_delete_api/403_test.go index e7265d12c4..33f85b3b99 100644 --- a/go/apps/api/routes/v2_apis_delete_api/403_test.go +++ b/go/apps/api/routes/v2_apis_delete_api/403_test.go @@ -1,118 +1,43 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_delete_api" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - Caches: h.Caches, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - api := h.CreateApi(seed.CreateApiRequest{WorkspaceID: h.Resources().UserWorkspace.ID}) - - // Test case for insufficient permissions - missing delete_api - t.Run("missing delete_api permission", func(t *testing.T) { - // Create a root key with only read_api but no delete_api permission - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: api.ID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - // Test case for permission for different API - t.Run("permission for different API", func(t *testing.T) { - // Create a root key with permissions for a specific different API - differentApiId := "api_different" - rootKey := h.CreateRootKey( - workspace.ID, - fmt.Sprintf("api.%s.delete_api", differentApiId), - ) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: api.ID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - // Test case for wrong workspace - t.Run("wrong workspace", func(t *testing.T) { - rootKey := h.CreateRootKey(uid.New(uid.WorkspacePrefix), "api.*.delete_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: api.ID, - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse]( - h, - route, - headers, - req, - ) - - // Note: We mask wrong workspace errors as 404 not found - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Equal(t, "The provided root key is invalid. The requested workspace does not exist.", res.Body.Error.Detail) - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Auditlogs: h.Auditlogs, + Caches: h.Caches, + } + }, + RequiredPermissions: []string{"api.*.delete_api"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + ApiId: res.ApiID, + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_apis_get_api/401_test.go b/go/apps/api/routes/v2_apis_get_api/401_test.go new file mode 100644 index 0000000000..1547189b4e --- /dev/null +++ b/go/apps/api/routes/v2_apis_get_api/401_test.go @@ -0,0 +1,28 @@ +package handler_test + +import ( + "testing" + + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_get_api" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +func TestGetApiUnauthorized(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + } + }, + func() handler.Request { + return handler.Request{ + ApiId: uid.New(uid.APIPrefix), + } + }, + ) +} diff --git a/go/apps/api/routes/v2_apis_get_api/403_test.go b/go/apps/api/routes/v2_apis_get_api/403_test.go index 498bc6e4d3..68093a660a 100644 --- a/go/apps/api/routes/v2_apis_get_api/403_test.go +++ b/go/apps/api/routes/v2_apis_get_api/403_test.go @@ -1,107 +1,41 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_get_api" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestGetApiInsufficientPermissions(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - } - - h.Register(route) - - api := h.CreateApi(seed.CreateApiRequest{WorkspaceID: h.Resources().UserWorkspace.ID}) - - testCases := []struct { - name string - permissions []string - }{ - { - name: "no permissions", - permissions: []string{}, - }, - { - name: "unrelated permission", - permissions: []string{"unrelated.permission"}, - }, - { - name: "wrong api permission", - permissions: []string{"api.*.create_api"}, - }, - { - name: "wrong scope for specific api", - permissions: []string{fmt.Sprintf("api.%s.create_api", api.ID)}, - }, - { - name: "permission for different api", - permissions: []string{fmt.Sprintf("api.%s.read_api", uid.New(uid.APIPrefix))}, - }, - { - name: "multiple insufficient permissions", - permissions: []string{"key.*.create", "ratelimit.*.create", "identity.*.read"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, tc.permissions...) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - handler.Request{ - ApiId: api.ID, - }, - ) - - require.Equal(t, 403, res.Status, "expected 403, received: %#v", res) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - } - - // Test with a valid key but from wrong workspace - t.Run("key from wrong workspace", func(t *testing.T) { - // Create another workspace - otherWorkspaceID := uid.New(uid.WorkspacePrefix) - - // Create key for other workspace with sufficient permissions - rootKey := h.CreateRootKey(otherWorkspaceID, "api.*.read_api") - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse]( - h, - route, - headers, - handler.Request{ - ApiId: api.ID, + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + } }, - ) - - require.Equal(t, 404, res.Status, "expected 404, received: %#v", res) - require.Equal(t, "https://unkey.com/docs/errors/unkey/data/workspace_not_found", res.Body.Error.Type) - require.Equal(t, "The provided root key is invalid. The requested workspace does not exist.", res.Body.Error.Detail) - }) + RequiredPermissions: []string{"api.*.read_api"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + ApiId: res.ApiID, + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_apis_list_keys/401_test.go b/go/apps/api/routes/v2_apis_list_keys/401_test.go index c019536f00..b62dee37fc 100644 --- a/go/apps/api/routes/v2_apis_list_keys/401_test.go +++ b/go/apps/api/routes/v2_apis_list_keys/401_test.go @@ -1,119 +1,30 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_list_keys" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Vault: h.Vault, - ApiCache: h.Caches.LiveApiByID, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - ApiId: "api_1234", - } - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - if res.Status == 401 { - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "key") - } - }) - - // Test case for expired or invalid key format - t.Run("invalid key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer not_a_valid_key"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "key") - }) - - // Test case for key with valid format but doesn't exist - t.Run("valid format non-existent key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer sk_test_1234567890abcdef1234567890abcdef"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "key") - }) - - // Test case for verifying error response structure - t.Run("verify error response structure", func(t *testing.T) { - // Use a clearly invalid token to ensure we get 401 - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer clearly_invalid_token_format"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.NotEmpty(t, res.Body.Error.Detail) - require.Equal(t, 401, res.Body.Error.Status) - require.NotEmpty(t, res.Body.Error.Title) - - // Verify meta information is included - require.NotNil(t, res.Body.Meta) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Vault: h.Vault, + ApiCache: h.Caches.LiveApiByID, + } + }, + func() handler.Request { + return handler.Request{ + ApiId: uid.New(uid.APIPrefix), + } + }, + ) } diff --git a/go/apps/api/routes/v2_apis_list_keys/403_test.go b/go/apps/api/routes/v2_apis_list_keys/403_test.go index 909c573e83..478cb468b1 100644 --- a/go/apps/api/routes/v2_apis_list_keys/403_test.go +++ b/go/apps/api/routes/v2_apis_list_keys/403_test.go @@ -1,380 +1,66 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_list_keys" - "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Vault: h.Vault, - ApiCache: h.Caches.LiveApiByID, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create a keyAuth (keyring) for the API - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: workspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false}, - DefaultBytes: sql.NullInt32{Valid: false}, - }) - require.NoError(t, err) - - err = db.Query.UpdateKeyringKeyEncryption(ctx, h.DB.RW(), db.UpdateKeyringKeyEncryptionParams{ - ID: keyAuthID, - StoreEncryptedKeys: true, - }) - require.NoError(t, err) - - // Create a test API - apiID := uid.New("api") - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: apiID, - Name: "Test API", - WorkspaceID: workspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: keyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Test case for insufficient permissions - missing read_key - t.Run("missing read_key permission", func(t *testing.T) { - // Create a root key with only read_api but no read_key permission - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for insufficient permissions - missing read_api - t.Run("missing read_api permission", func(t *testing.T) { - // Create a root key with only read_key but no read_api permission - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for permission for different API - t.Run("permission for different API", func(t *testing.T) { - // Create a root key with permissions for a specific different API - differentApiId := "api_different_123" - rootKey := h.CreateRootKey( - workspace.ID, - fmt.Sprintf("api.%s.read_key", differentApiId), - fmt.Sprintf("api.%s.read_api", differentApiId), - ) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, // Using the test API, not the one we have permission for - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for decrypt permission - t.Run("missing decrypt permission", func(t *testing.T) { - // Create a root key with read permissions but no decrypt permission - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key", "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - decrypt := true - req := handler.Request{ - ApiId: apiID, - Decrypt: &decrypt, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for no permissions at all - t.Run("no permissions", func(t *testing.T) { - // Create a root key with no relevant permissions - rootKey := h.CreateRootKey(workspace.ID, "workspace.read") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for cross-workspace access attempt - t.Run("cross workspace access", func(t *testing.T) { - // Create a different workspace - differentWorkspace := h.CreateWorkspace() - - // Create a root key for the different workspace with full permissions - rootKey := h.CreateRootKey(differentWorkspace.ID, "api.*.read_key", "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, // API belongs to the original workspace - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - // Should return 404 (not found) rather than 403 for security reasons - // The system masks cross-workspace access as "not found" - require.True(t, res.Status == 403 || res.Status == 404) - }) - - // Test case for wildcard permissions (should work) - t.Run("wildcard permissions should work", func(t *testing.T) { - // Create a root key with explicit wildcard API permissions for both required actions - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key", "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, handler.Response]( - h, - route, - headers, - req, - ) - - // Should not be 403 - wildcard API permissions should work - require.NotEqual(t, 403, res.Status) - // Should be 200 (success) assuming the API exists and has keys to list - require.True(t, res.Status == 200 || res.Status == 404) - }) - - // Test case for specific API permissions (should work) - t.Run("specific API permissions should work", func(t *testing.T) { - // Create a root key with permissions for this specific API - rootKey := h.CreateRootKey(workspace.ID, - fmt.Sprintf("api.%s.read_key", apiID), - fmt.Sprintf("api.%s.read_api", apiID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, handler.Response]( - h, - route, - headers, - req, - ) - - // Should not be 403 - specific permissions should work - require.NotEqual(t, 403, res.Status) - // Should be 200 (success) - require.Equal(t, 200, res.Status) - }) - - // Test case for verifying error response structure - t.Run("verify error response structure", func(t *testing.T) { - // Create a root key with insufficient permissions - rootKey := h.CreateRootKey(workspace.ID, "workspace.read") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.NotEmpty(t, res.Body.Error.Detail) - require.Equal(t, 403, res.Body.Error.Status) - require.NotEmpty(t, res.Body.Error.Title) - - // Verify meta information is included - require.NotNil(t, res.Body.Meta) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - // Test case for partial permissions (read_api but not read_key) - t.Run("partial permissions insufficient", func(t *testing.T) { - // Create a root key with only one of the required permissions - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - ApiId: apiID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for decrypt permission with wildcard - t.Run("decrypt with wildcard permission should work", func(t *testing.T) { - // Create a root key with wildcard API permissions (includes decrypt) - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key", "api.*.read_api", "api.*.decrypt_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - decrypt := true - req := handler.Request{ - ApiId: apiID, - Decrypt: &decrypt, - } - - res := testutil.CallRoute[handler.Request, handler.Response]( - h, - route, - headers, - req, - ) - - // Should not be 403 - wildcard API permissions should include decrypt - require.NotEqual(t, 403, res.Status) - // Should be 200 (success) - require.Equal(t, 200, res.Status) - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + Logger: h.Logger, + DB: h.DB, + Keys: h.Keys, + Vault: h.Vault, + ApiCache: h.Caches.LiveApiByID, + } + }, + RequiredPermissions: []string{"api.*.read_key", "api.*.read_api"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + ApiId: res.ApiID, + } + }, + AdditionalPermissionTests: []authz.PermissionTestCase[handler.Request]{ + { + Name: "missing read_key permission", + Permissions: []string{"api.*.read_api"}, + ExpectedStatus: 403, + }, + { + Name: "missing read_api permission", + Permissions: []string{"api.*.read_key"}, + ExpectedStatus: 403, + }, + { + Name: "decrypt without decrypt_key permission", + Permissions: []string{"api.*.read_key", "api.*.read_api"}, + ModifyRequest: func(req handler.Request, res authz.TestResources) handler.Request { + req.Decrypt = ptr.P(true) + return req + }, + ExpectedStatus: 403, + }, + }, + }, + ) } diff --git a/go/apps/api/routes/v2_identities_create_identity/401_test.go b/go/apps/api/routes/v2_identities_create_identity/401_test.go index 1c08398497..e61e9510d1 100644 --- a/go/apps/api/routes/v2_identities_create_identity/401_test.go +++ b/go/apps/api/routes/v2_identities_create_identity/401_test.go @@ -1,38 +1,28 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_create_identity" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUnauthorizedAccess(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ExternalId: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.NotNil(t, res.Body) - }) - +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + ExternalId: "test_external_id", + } + }, + ) } diff --git a/go/apps/api/routes/v2_identities_create_identity/403_test.go b/go/apps/api/routes/v2_identities_create_identity/403_test.go index 7f40de0960..d37805a347 100644 --- a/go/apps/api/routes/v2_identities_create_identity/403_test.go +++ b/go/apps/api/routes/v2_identities_create_identity/403_test.go @@ -1,36 +1,31 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_create_identity" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestWorkspacePermissions(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ExternalId: "external_test_id"} - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "got: %s", res.RawBody) - require.NotNil(t, res.Body) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"identity.*.create_identity"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + ExternalId: "test_external_id", + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_identities_delete_identity/401_test.go b/go/apps/api/routes/v2_identities_delete_identity/401_test.go index 4a8c34ff32..b0228327b6 100644 --- a/go/apps/api/routes/v2_identities_delete_identity/401_test.go +++ b/go/apps/api/routes/v2_identities_delete_identity/401_test.go @@ -1,179 +1,29 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_delete_identity" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestDeleteIdentityUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - // No Authorization header - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status, "expected 400, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) - require.Equal(t, "Bad Request", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header"}, - } - - req := handler.Request{ - Identity: uid.New("test"), - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status, "expected 400, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/malformed", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "Bearer") - require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) - require.Equal(t, "Bad Request", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("invalid auth token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status, "expected 401, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/key_not_found", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "key") - require.Equal(t, http.StatusUnauthorized, res.Body.Error.Status) - require.Equal(t, "Unauthorized", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("empty bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - - req := handler.Request{ - Identity: uid.New("test"), - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status, "expected 400, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/malformed", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "Bearer") - require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) - require.Equal(t, "Bad Request", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("bearer token with invalid format", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer not-a-valid-key-format"}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status, "expected 401, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/key_not_found", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "key") - require.Equal(t, http.StatusUnauthorized, res.Body.Error.Status) - require.Equal(t, "Unauthorized", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("token with wrong format", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer random_string_not_a_key"}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status, "expected 401, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/key_not_found", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "key") - require.Equal(t, http.StatusUnauthorized, res.Body.Error.Status) - require.Equal(t, "Unauthorized", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("key from wrong workspace", func(t *testing.T) { - // Create a different workspace - differentWorkspace := h.CreateWorkspace() - - // Create a root key for different workspace - differentWorkspaceKey := h.CreateRootKey(differentWorkspace.ID, "identity.*.delete_identity") - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusNotFound, res.Status, "expected 404, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/data/identity_not_found", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "identity does not exist") - require.Equal(t, http.StatusNotFound, res.Body.Error.Status) - require.Equal(t, "Not Found", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("completely invalid token format", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer 123"}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status, "expected 401, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/key_not_found", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "key") - require.Equal(t, http.StatusUnauthorized, res.Body.Error.Status) - require.Equal(t, "Unauthorized", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Identity: uid.New("test"), + } + }, + ) } diff --git a/go/apps/api/routes/v2_identities_delete_identity/403_test.go b/go/apps/api/routes/v2_identities_delete_identity/403_test.go index 524b5edad0..2e9212fa96 100644 --- a/go/apps/api/routes/v2_identities_delete_identity/403_test.go +++ b/go/apps/api/routes/v2_identities_delete_identity/403_test.go @@ -1,164 +1,32 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_delete_identity" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestDeleteIdentityForbidden(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("insufficient permissions - no permissions", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) // No permissions - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("insufficient permissions - wrong permission", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.create_identity") // Wrong permission - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("insufficient permissions - different resource permission", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "key.*.delete_key") // Different resource type - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("read-only permission", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.read_identity") // Read permission instead of delete - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("partial permission match", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.create_identity") // Missing wildcard/specific ID - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("multiple permissions but none matching", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, - "key.*.delete_key", - "api.*.delete_api", - "workspace.*.read_workspace") // Multiple permissions but none for identity deletion - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Identity: uid.New("test"), - } - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) - - t.Run("case sensitivity test", func(t *testing.T) { - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "IDENTITY.*.DELETE_IDENTITY") // Wrong case - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{Identity: uid.New("test")} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, sent: %+v, received: %s", req, res.RawBody) - require.NotNil(t, res.Body) - - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - require.Equal(t, http.StatusForbidden, res.Body.Error.Status) - require.Equal(t, "Insufficient Permissions", res.Body.Error.Title) - require.NotEmpty(t, res.Body.Meta.RequestId) - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"identity.*.delete_identity"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Identity: uid.New("test"), + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_identities_get_identity/401_test.go b/go/apps/api/routes/v2_identities_get_identity/401_test.go index 9e6daac74c..66361a6c57 100644 --- a/go/apps/api/routes/v2_identities_get_identity/401_test.go +++ b/go/apps/api/routes/v2_identities_get_identity/401_test.go @@ -1,37 +1,27 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_get_identity" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - } - - h.Register(route) - - t.Run("invalid root key", func(t *testing.T) { - req := handler.Request{ - Identity: "identity_123", - } - - // Non-existent key - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key"}, - } - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/key_not_found", res.Body.Error.Type) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{ + Identity: "test_identity", + } + }, + ) } diff --git a/go/apps/api/routes/v2_identities_get_identity/403_test.go b/go/apps/api/routes/v2_identities_get_identity/403_test.go index cd21a6f5ee..bc37b58aef 100644 --- a/go/apps/api/routes/v2_identities_get_identity/403_test.go +++ b/go/apps/api/routes/v2_identities_get_identity/403_test.go @@ -1,100 +1,43 @@ package handler_test import ( - "context" - "fmt" - "net/http" - "regexp" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_get_identity" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestForbidden(t *testing.T) { - h := testutil.NewHarness(t) - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - } - - h.Register(route) - - // Create a root key with no permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - // Create test identity - ctx := context.Background() - tx, err := h.DB.RW().Begin(ctx) - require.NoError(t, err) - defer tx.Rollback() - - workspaceID := h.Resources().UserWorkspace.ID - identityID := uid.New(uid.IdentityPrefix) - otherIdentityID := uid.New(uid.IdentityPrefix) - externalID := "test_user_403" - - // Insert test identity - err = db.Query.InsertIdentity(ctx, tx, db.InsertIdentityParams{ - ID: identityID, - ExternalID: externalID, - WorkspaceID: workspaceID, - Environment: "default", - CreatedAt: time.Now().UnixMilli(), - Meta: []byte("{}"), - }) - require.NoError(t, err) - - // Insert another test identity - err = db.Query.InsertIdentity(ctx, tx, db.InsertIdentityParams{ - ID: otherIdentityID, - ExternalID: "other_user_403", - WorkspaceID: workspaceID, - Environment: "default", - CreatedAt: time.Now().UnixMilli(), - Meta: []byte("{}"), - }) - require.NoError(t, err) - - err = tx.Commit() - require.NoError(t, err) - - t.Run("no permission to read any identity", func(t *testing.T) { - // The rootKey has no permissions, so it should fail - req := handler.Request{ - Identity: externalID, - } - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Regexp(t, regexp.MustCompile(`^Missing one of these permissions: \[.*\], have: \[.*\]$`), res.Body.Error.Detail) - }) - - t.Run("permission by external ID but not by ID", func(t *testing.T) { - // Create a key with specific identity permission - specificPermKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity."+otherIdentityID+".read_identity") - specificHeaders := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", specificPermKey)}, - } - - // Try to use identity when only having permission for specific identity IDs - req := handler.Request{ - Identity: externalID, - } - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, specificHeaders, req) - require.Equal(t, http.StatusForbidden, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Regexp(t, regexp.MustCompile(`^Missing one of these permissions: \[.*\], have: \[.*\]$`), res.Body.Error.Detail) - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"identity.*.read_identity"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + identityID := h.CreateIdentity(seed.CreateIdentityRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ExternalID: "test_identity", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "identity_id": identityID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Identity: res.Custom["identity_id"], + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_identities_list_identities/401_test.go b/go/apps/api/routes/v2_identities_list_identities/401_test.go index e0c811411e..3049a5c3e7 100644 --- a/go/apps/api/routes/v2_identities_list_identities/401_test.go +++ b/go/apps/api/routes/v2_identities_list_identities/401_test.go @@ -1,60 +1,25 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_list_identities" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - } - - // Register the route with the harness - h.Register(route) - - t.Run("missing Authorization header", func(t *testing.T) { - req := handler.Request{} - - // Call without auth header - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, nil, req) - require.Equal(t, http.StatusBadRequest, res.Status) - // The specific error type may vary, so we just check it's a valid error response - require.NotEmpty(t, res.Body.Error.Type) - require.NotEmpty(t, res.Body.Error.Detail) - }) - - t.Run("malformed Authorization header", func(t *testing.T) { - req := handler.Request{} - - // Invalid format - headers := http.Header{ - "Authorization": []string{"InvalidFormat xyz"}, - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status) - // The specific error type may vary, so we just check it's a valid error response - require.NotEmpty(t, res.Body.Error.Type) - }) - - t.Run("invalid root key", func(t *testing.T) { - req := handler.Request{} - - // Non-existent key - headers := http.Header{ - "Authorization": []string{"Bearer invalid_key"}, - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status) - // The specific error type may vary, so we just check it's a valid error response - require.NotEmpty(t, res.Body.Error.Type) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{} + }, + ) } diff --git a/go/apps/api/routes/v2_identities_list_identities/403_test.go b/go/apps/api/routes/v2_identities_list_identities/403_test.go index cd7f0f50ec..d983a81f3c 100644 --- a/go/apps/api/routes/v2_identities_list_identities/403_test.go +++ b/go/apps/api/routes/v2_identities_list_identities/403_test.go @@ -1,159 +1,43 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_list_identities" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestForbidden(t *testing.T) { - h := testutil.NewHarness(t) - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - } - - // Create a rootKey without any permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - // Create test identities in different environments - ctx := context.Background() - tx, err := h.DB.RW().Begin(ctx) - require.NoError(t, err) - defer tx.Rollback() - - workspaceID := h.Resources().UserWorkspace.ID - - // Insert identity in default environment - defaultIdentityID := uid.New(uid.IdentityPrefix) - err = db.Query.InsertIdentity(ctx, tx, db.InsertIdentityParams{ - ID: defaultIdentityID, - ExternalID: "test_user_default", - WorkspaceID: workspaceID, - Environment: "default", - CreatedAt: time.Now().UnixMilli(), - Meta: []byte("{}"), - }) - require.NoError(t, err) - - // Insert identity in production environment - prodIdentityID := uid.New(uid.IdentityPrefix) - err = db.Query.InsertIdentity(ctx, tx, db.InsertIdentityParams{ - ID: prodIdentityID, - ExternalID: "test_user_prod", - WorkspaceID: workspaceID, - Environment: "production", - CreatedAt: time.Now().UnixMilli(), - Meta: []byte("{}"), - }) - require.NoError(t, err) - - // Insert identity in staging environment - stagingIdentityID := uid.New(uid.IdentityPrefix) - err = db.Query.InsertIdentity(ctx, tx, db.InsertIdentityParams{ - ID: stagingIdentityID, - ExternalID: "test_user_staging", - WorkspaceID: workspaceID, - Environment: "staging", - CreatedAt: time.Now().UnixMilli(), - Meta: []byte("{}"), - }) - require.NoError(t, err) - - err = tx.Commit() - require.NoError(t, err) - - // Register the route - h.Register(route) - - t.Run("no permission to read any identity", func(t *testing.T) { - // With no permissions set, should return 403 - req := handler.Request{} - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions") - }) - - // Create a new key with specific permissions for certain environments - t.Run("with permission for only specific environment", func(t *testing.T) { - // Create a new key with production environment permissions - prodPermKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.read_identity") - prodHeaders := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", prodPermKey)}, - } - - // Attempt to list identities (should see all identities with wildcard permission) - req := handler.Request{} - res := testutil.CallRoute[handler.Request, handler.Response](h, route, prodHeaders, req) - - // Should get a 200 response with all identities - require.Equal(t, http.StatusOK, res.Status) - - // Verify we can see all identities including production - foundProd := false - for _, identity := range res.Body.Data { - // Should be able to see production identity - if identity.ExternalId == "test_user_prod" { - foundProd = true - } - } - - require.True(t, foundProd, "Should find production identity") - }) - - t.Run("with wildcard permission", func(t *testing.T) { - // Create a new key with wildcard permissions - wildcardKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.read_identity") - wildcardHeaders := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", wildcardKey)}, - } - - // Attempt to list identities - req := handler.Request{} - res := testutil.CallRoute[handler.Request, handler.Response](h, route, wildcardHeaders, req) - - // Should get a 200 response with all identities - require.Equal(t, http.StatusOK, res.Status) - - // Should see at least 3 identities (one from each environment) - require.GreaterOrEqual(t, len(res.Body.Data), 3) - - // Verify we can find all environment identities - foundDefault := false - foundProd := false - foundStaging := false - - for _, identity := range res.Body.Data { - if identity.ExternalId == "test_user_default" { - foundDefault = true - } - if identity.ExternalId == "test_user_prod" { - foundProd = true - } - if identity.ExternalId == "test_user_staging" { - foundStaging = true - } - } - - require.True(t, foundDefault, "Should find default environment identity") - require.True(t, foundProd, "Should find production environment identity") - require.True(t, foundStaging, "Should find staging environment identity") - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"identity.*.read_identity"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + // Create test identities so permission check is triggered + h.CreateIdentity(seed.CreateIdentityRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ExternalID: "test_identity_1", + }) + h.CreateIdentity(seed.CreateIdentityRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ExternalID: "test_identity_2", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{} + }, + }, + ) } diff --git a/go/apps/api/routes/v2_identities_update_identity/401_test.go b/go/apps/api/routes/v2_identities_update_identity/401_test.go index 08d43d371a..83294a752e 100644 --- a/go/apps/api/routes/v2_identities_update_identity/401_test.go +++ b/go/apps/api/routes/v2_identities_update_identity/401_test.go @@ -1,129 +1,34 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_update_identity" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("missing Authorization header", func(t *testing.T) { - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - - // Call without auth header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - }) - - t.Run("malformed Authorization header", func(t *testing.T) { - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - - // Invalid format - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"InvalidFormat xyz"}, - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/malformed", res.Body.Error.Type) - }) - - t.Run("invalid root key", func(t *testing.T) { - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - - // Non-existent key - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key"}, - } - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/key_not_found", res.Body.Error.Type) - }) - - t.Run("empty bearer token", func(t *testing.T) { - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusBadRequest, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authentication/malformed", res.Body.Error.Type) - }) - - t.Run("key from different workspace", func(t *testing.T) { - // Create a different workspace - differentWorkspace := h.CreateWorkspace() - - // Create a root key for different workspace - differentWorkspaceKey := h.CreateRootKey(differentWorkspace.ID, "identity.*.update_identity") - - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusNotFound, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/data/identity_not_found", res.Body.Error.Type) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + externalID := uid.New(uid.TestPrefix) + meta := map[string]interface{}{ + "test": "value", + } + return handler.Request{ + Identity: externalID, + Meta: &meta, + } + }, + ) } diff --git a/go/apps/api/routes/v2_identities_update_identity/403_test.go b/go/apps/api/routes/v2_identities_update_identity/403_test.go index b3bdcaad40..06bbddd55d 100644 --- a/go/apps/api/routes/v2_identities_update_identity/403_test.go +++ b/go/apps/api/routes/v2_identities_update_identity/403_test.go @@ -1,115 +1,48 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_identities_update_identity" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestForbidden(t *testing.T) { - h := testutil.NewHarness(t) - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("no permission to update identity", func(t *testing.T) { - // Create root key without permissions - rootKeyID := h.CreateRootKey(h.Resources().UserWorkspace.ID) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyID)}, - } - - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - t.Run("wrong permission type", func(t *testing.T) { - // Create root key with wrong permission - rootKeyID := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.create_identity") - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyID)}, - } - - externalID := uid.New(uid.TestPrefix) - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, http.StatusForbidden, res.Status) - require.Equal(t, "https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions", res.Body.Error.Type) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - t.Run("with permission to update identity", func(t *testing.T) { - // Create test identity first - ctx := context.Background() - tx, err := h.DB.RW().Begin(ctx) - require.NoError(t, err) - defer tx.Rollback() - - workspaceID := h.Resources().UserWorkspace.ID - identityID := uid.New(uid.IdentityPrefix) - externalID := "test_user_403" - - // Insert test identity - err = db.Query.InsertIdentity(ctx, tx, db.InsertIdentityParams{ - ID: identityID, - ExternalID: externalID, - WorkspaceID: workspaceID, - Environment: "default", - CreatedAt: time.Now().UnixMilli(), - Meta: []byte("{}"), - }) - require.NoError(t, err) - err = tx.Commit() - require.NoError(t, err) - - // Create root key with correct permission - rootKeyID := h.CreateRootKey(workspaceID, "identity.*.update_identity") - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyID)}, - } - - meta := map[string]interface{}{ - "test": "value", - } - req := handler.Request{ - Identity: externalID, - Meta: &meta, - } - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusOK, res.Status, "expected 200, got: %d, response: %s", res.Status, res.RawBody) - require.Equal(t, externalID, res.Body.Data.ExternalId) - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"identity.*.update_identity"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + identityID := h.CreateIdentity(seed.CreateIdentityRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ExternalID: "test_identity", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "identity_id": identityID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + meta := map[string]interface{}{ + "test": "value", + } + return handler.Request{ + Identity: res.Custom["identity_id"], + Meta: &meta, + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_add_permissions/401_test.go b/go/apps/api/routes/v2_keys_add_permissions/401_test.go index 2d1449cd75..0427efd6b6 100644 --- a/go/apps/api/routes/v2_keys_add_permissions/401_test.go +++ b/go/apps/api/routes/v2_keys_add_permissions/401_test.go @@ -1,161 +1,31 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_add_permissions" - "github.com/unkeyed/unkey/go/pkg/db" - dbtype "github.com/unkeyed/unkey/go/pkg/db/types" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - IpWhitelist: "", - EncryptedKeys: false, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - key := h.CreateKey(seed.CreateKeyRequest{ - Disabled: false, - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Remaining: nil, - IdentityID: nil, - Meta: nil, - }) - - permissionID := uid.New(uid.TestPrefix) - err := db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: permissionID, - WorkspaceID: workspace.ID, - Name: "documents.read.auth", - Slug: "documents.read.auth", - Description: dbtype.NullString{Valid: true, String: "Read documents permission"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: key.KeyID, - Permissions: []string{permissionID}, - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - }) - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "key") - }) - - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_format"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "valid root key") - }) - - t.Run("empty bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "valid root key") - }) - - t.Run("disabled root key", func(t *testing.T) { - // Use invalid root key to simulate disabled key - rootKey := "invalid_disabled_key" - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Permissions: []string{"test.permission"}, + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_add_permissions/403_test.go b/go/apps/api/routes/v2_keys_add_permissions/403_test.go index 89ae4e4da4..16fa4ed289 100644 --- a/go/apps/api/routes/v2_keys_add_permissions/403_test.go +++ b/go/apps/api/routes/v2_keys_add_permissions/403_test.go @@ -1,195 +1,60 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_add_permissions" - "github.com/unkeyed/unkey/go/pkg/db" - dbtype "github.com/unkeyed/unkey/go/pkg/db/types" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - IpWhitelist: "", - EncryptedKeys: false, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - key := h.CreateKey(seed.CreateKeyRequest{ - Disabled: false, - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Remaining: nil, - IdentityID: nil, - Meta: nil, - Expires: nil, - Name: nil, - Deleted: false, - RefillAmount: nil, - RefillDay: nil, - Permissions: nil, - Roles: nil, - Ratelimits: nil, - }) - - permissionID := uid.New(uid.TestPrefix) - err := db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: permissionID, - WorkspaceID: workspace.ID, - Name: "documents.read.auth403", - Slug: "documents.read.auth403", - Description: dbtype.NullString{Valid: true, String: "Read documents permission"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: key.KeyID, - Permissions: []string{permissionID}, - } - - t.Run("root key without required permissions", func(t *testing.T) { - // Create root key without the required permission - insufficientRootKey := h.CreateRootKey(workspace.ID, "some.other.permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", insufficientRootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - t.Run("root key with partial permissions", func(t *testing.T) { - // Create root key with related but insufficient permission - partialRootKey := h.CreateRootKey(workspace.ID, "api.read.update_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", partialRootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - t.Run("key belongs to different workspace", func(t *testing.T) { - diffWorkspace := h.CreateWorkspace() - - diffApi := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: diffWorkspace.ID, - IpWhitelist: "", - EncryptedKeys: false, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - diffKey := h.CreateKey(seed.CreateKeyRequest{ - Disabled: false, - WorkspaceID: diffWorkspace.ID, - KeyAuthID: diffApi.KeyAuthID.String, - Remaining: nil, - IdentityID: nil, - Meta: nil, - Expires: nil, - Name: nil, - Deleted: false, - RefillAmount: nil, - RefillDay: nil, - Permissions: nil, - Roles: nil, - Ratelimits: nil, - }) - - // Create root key for original workspace (authorized for workspace.ID, not otherWorkspaceID) - authorizedRootKey := h.CreateRootKey(workspace.ID, "api.*.update_key", "rbac.*.add_permission_to_key") - - reqWithOtherKey := handler.Request{ - KeyId: diffKey.KeyID, - Permissions: []string{permissionID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", authorizedRootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse]( - h, - route, - headers, - reqWithOtherKey, - ) - - require.Equal(t, 404, res.Status) // Key not found (because it belongs to different workspace) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "key was not found") - }) - - t.Run("root key with no permissions", func(t *testing.T) { - // Create root key with no permissions - noPermissionsRootKey := h.CreateRootKey(workspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", noPermissionsRootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "permission") - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + RequiredPermissions: []string{"api.*.update_key", "rbac.*.add_permission_to_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + permID := h.CreatePermission(seed.CreatePermissionRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "documents.read", + Slug: "documents-read", + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + Custom: map[string]string{ + "permission_id": permID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Permissions: []string{res.Custom["permission_id"]}, + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_add_roles/401_test.go b/go/apps/api/routes/v2_keys_add_roles/401_test.go index d73e4830ad..1805fce184 100644 --- a/go/apps/api/routes/v2_keys_add_roles/401_test.go +++ b/go/apps/api/routes/v2_keys_add_roles/401_test.go @@ -1,191 +1,31 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_add_roles" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace and valid key for the request - workspace := h.Resources().UserWorkspace - - // Create a test API and key using testutil helper - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a valid request - req := handler.Request{ - KeyId: keyID, - Roles: []string{"role_123"}, - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for empty authorization header - t.Run("empty authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {""}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for Bearer token only (no actual token) - t.Run("bearer token only", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for non-existent root key with valid format - t.Run("non-existent root key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer unkey_32kHz9hXEXWMa8qGpTLSgzTD5Q"}, // Valid format but non-existent - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for disabled root key - t.Run("disabled root key", func(t *testing.T) { - // Use invalid root key to simulate disabled key - rootKey := "invalid_disabled_key" - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Roles: []string{"test.role"}, + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_add_roles/403_test.go b/go/apps/api/routes/v2_keys_add_roles/403_test.go index df50d530ff..024573caae 100644 --- a/go/apps/api/routes/v2_keys_add_roles/403_test.go +++ b/go/apps/api/routes/v2_keys_add_roles/403_test.go @@ -1,235 +1,59 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_add_roles" - "github.com/unkeyed/unkey/go/pkg/db" - "github.com/unkeyed/unkey/go/pkg/hash" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - Logger: h.Logger, - DB: h.DB, - Keys: h.Keys, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Test case for insufficient permissions - t.Run("insufficient permissions - no update_key", func(t *testing.T) { - // Create a workspace and root key without UpdateKey permission - workspace := h.Resources().UserWorkspace - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key") // Only read permission - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - req := handler.Request{ - KeyId: keyID, - Roles: []string{"role_123"}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions") - }) - - // Test case for cross workspace access - t.Run("cross workspace access", func(t *testing.T) { - // Create two different workspaces - workspace1 := h.Resources().UserWorkspace - workspace2 := h.CreateWorkspace() - - // Root key from workspace1 - rootKey := h.CreateRootKey(workspace1.ID, "api.*.update_key") - - // Create a test keyring in workspace2 - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: workspace2.ID, - StoreEncryptedKeys: false, - DefaultPrefix: sql.NullString{Valid: true, String: "test"}, - DefaultBytes: sql.NullInt32{Valid: true, Int32: 16}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create a test key in workspace2 - keyID := uid.New(uid.KeyPrefix) - keyString := "test_" + uid.New("") - err = db.Query.InsertKey(ctx, h.DB.RW(), db.InsertKeyParams{ - ID: keyID, - KeyringID: keyAuthID, - Hash: hash.Sha256(keyString), - Start: keyString[:4], - WorkspaceID: workspace2.ID, - ForWorkspaceID: sql.NullString{Valid: false}, - Name: sql.NullString{Valid: true, String: "Test Key"}, - CreatedAtM: time.Now().UnixMilli(), - Enabled: true, - IdentityID: sql.NullString{Valid: false}, - Meta: sql.NullString{Valid: false}, - Expires: sql.NullTime{Valid: false}, - RemainingRequests: sql.NullInt32{Valid: false}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{"role_123"}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "The specified key was not found") - }) - - // Test case for root key with only read permissions - t.Run("root key with read only permissions", func(t *testing.T) { - workspace := h.Resources().UserWorkspace - // Create root key with only read permissions - rootKey := h.CreateRootKey(workspace.ID, "api.*.read_key", "rbac.*.read_role") - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - req := handler.Request{ - KeyId: keyID, - Roles: []string{"role_123"}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions") - }) - - // Test case for expired root key - t.Run("expired root key", func(t *testing.T) { - workspace := h.Resources().UserWorkspace - // Use invalid root key to simulate expired key - rootKey := "expired_root_key_12345" - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - req := handler.Request{ - KeyId: keyID, - Roles: []string{"role_123"}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + RequiredPermissions: []string{"api.*.update_key", "rbac.*.add_role_to_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + roleID := h.CreateRole(seed.CreateRoleRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test-role", + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + Custom: map[string]string{ + "role_id": roleID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Roles: []string{res.Custom["role_id"]}, + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_create_key/401_test.go b/go/apps/api/routes/v2_keys_create_key/401_test.go index 8ccd2b9846..3c188fd5c9 100644 --- a/go/apps/api/routes/v2_keys_create_key/401_test.go +++ b/go/apps/api/routes/v2_keys_create_key/401_test.go @@ -1,67 +1,31 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_create_key" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestCreateKeyUnauthorized(t *testing.T) { - - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - // Basic request body - req := handler.Request{ - ApiId: uid.New(uid.APIPrefix), - } - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key_12345"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("nonexistent key", func(t *testing.T) { - nonexistentKey := uid.New(uid.KeyPrefix) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", nonexistentKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("bearer with extra spaces", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key_with_spaces "}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + func() handler.Request { + // Using a placeholder API ID - auth check happens before API validation + return handler.Request{ + ApiId: uid.New(uid.APIPrefix), + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_create_key/403_test.go b/go/apps/api/routes/v2_keys_create_key/403_test.go index 08a20a4cfc..bf4676bb10 100644 --- a/go/apps/api/routes/v2_keys_create_key/403_test.go +++ b/go/apps/api/routes/v2_keys_create_key/403_test.go @@ -1,192 +1,97 @@ package handler_test import ( - "context" - "database/sql" "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_create_key" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestCreateKeyForbidden(t *testing.T) { - - h := testutil.NewHarness(t) - ctx := context.Background() - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - // Create API for testing - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: h.Resources().UserWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - err = db.Query.UpdateKeyringKeyEncryption(ctx, h.DB.RW(), db.UpdateKeyringKeyEncryptionParams{ - ID: keyAuthID, - StoreEncryptedKeys: true, - }) - require.NoError(t, err) - - apiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: apiID, - Name: "test-api", - WorkspaceID: h.Resources().UserWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: keyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create another API for cross-API testing - otherKeyAuthID := uid.New(uid.KeyAuthPrefix) - err = db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: otherKeyAuthID, - WorkspaceID: h.Resources().UserWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - otherApiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: otherApiID, - Name: "other-api", - WorkspaceID: h.Resources().UserWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: otherKeyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - req := handler.Request{ - ApiId: apiID, - } - - t.Run("no permissions", func(t *testing.T) { - // Create root key with no permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong permission - has read but not create", func(t *testing.T) { - // Create root key with read permission instead of create - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.read_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + RequiredPermissions: []string{"api.*.create_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + // Create primary API for testing + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + ApiId: res.ApiID, + } + }, + AdditionalPermissionTests: []authz.PermissionTestCase[handler.Request]{ + { + Name: "create recoverable key without encrypt_key permission", + Permissions: []string{"api.*.create_key"}, // Has create_key but not encrypt_key + ModifyRequest: func(req handler.Request, res authz.TestResources) handler.Request { + req.Recoverable = ptr.P(true) + return req + }, + ExpectedStatus: 403, + }, + }, + }, + ) + + // Cross-API permission test t.Run("permission for different API", func(t *testing.T) { - // Create root key with create permission for other API - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.create_key", otherApiID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + h := testutil.NewHarness(t) + + route := &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, } + h.Register(route) - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("permission for specific API but requesting different API", func(t *testing.T) { - // Create root key with create permission for specific API - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.create_key", otherApiID)) + // Create two APIs + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + otherApi := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } + // Create root key with permission for otherApi only + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.create_key", otherApi.ID)) // Try to create key for different API - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("unrelated permission", func(t *testing.T) { - // Create root key with completely unrelated permission - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "workspace.read") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("partial permission match", func(t *testing.T) { - // Create root key with permission that partially matches but isn't sufficient - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.create") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("create recoverable key without perms", func(t *testing.T) { - // Create root key with permission that partially matches but isn't sufficient - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") - req := handler.Request{ - ApiId: apiID, - Recoverable: ptr.P(true), + ApiId: api.ID, } - headers := http.Header{ + res := testutil.CallRoute[handler.Request, handler.Response](h, route, map[string][]string{ "Content-Type": {"application/json"}, "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } + }, req) - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) + if res.Status != 403 { + t.Errorf("expected 403, got %d, body: %s", res.Status, res.RawBody) + } }) } diff --git a/go/apps/api/routes/v2_keys_delete_key/401_test.go b/go/apps/api/routes/v2_keys_delete_key/401_test.go index f95eac9430..511d8e8c36 100644 --- a/go/apps/api/routes/v2_keys_delete_key/401_test.go +++ b/go/apps/api/routes/v2_keys_delete_key/401_test.go @@ -1,144 +1,30 @@ package handler_test import ( - "database/sql" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_delete_key" - "github.com/unkeyed/unkey/go/internal/services/keys" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestKeyDeleteUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - ctx := t.Context() - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace and user - workspace := h.Resources().UserWorkspace - - // Create a keyAuth (keyring) for the API - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: workspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false}, - DefaultBytes: sql.NullInt32{Valid: false}, - }) - require.NoError(t, err) - - // Create a test API - apiID := uid.New("api") - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: apiID, - Name: "Test API", - WorkspaceID: workspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: keyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - keyID := uid.New(uid.KeyPrefix) - key, _ := h.Keys.CreateKey(ctx, keys.CreateKeyRequest{ - Prefix: "test", - ByteLength: 16, - }) - - insertParams := db.InsertKeyParams{ - ID: keyID, - KeyringID: keyAuthID, - Hash: key.Hash, - Start: key.Start, - WorkspaceID: workspace.ID, - ForWorkspaceID: sql.NullString{Valid: false}, - Name: sql.NullString{Valid: true, String: "test-key"}, - Expires: sql.NullTime{Valid: false}, - CreatedAtM: time.Now().UnixMilli(), - Enabled: true, - IdentityID: sql.NullString{Valid: false, String: ""}, - RemainingRequests: sql.NullInt32{Int32: 100, Valid: true}, - } - - err = db.Query.InsertKey(ctx, h.DB.RW(), insertParams) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("empty authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {""}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - no Bearer prefix", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_token_without_bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - Bearer only", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("nonexistent root key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer " + uid.New(uid.KeyPrefix)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_delete_key/403_test.go b/go/apps/api/routes/v2_keys_delete_key/403_test.go index 4d64da0927..ba35d1acad 100644 --- a/go/apps/api/routes/v2_keys_delete_key/403_test.go +++ b/go/apps/api/routes/v2_keys_delete_key/403_test.go @@ -1,186 +1,113 @@ package handler_test import ( - "context" - "database/sql" "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_delete_key" - "github.com/unkeyed/unkey/go/pkg/db" - "github.com/unkeyed/unkey/go/pkg/hash" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestKeyDeleteForbidden(t *testing.T) { - - h := testutil.NewHarness(t) - ctx := context.Background() - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create API for testing - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: h.Resources().UserWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - apiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: apiID, - Name: "test-api", - WorkspaceID: h.Resources().UserWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: keyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create another API for cross-API testing - otherKeyAuthID := uid.New(uid.KeyAuthPrefix) - err = db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: otherKeyAuthID, - WorkspaceID: h.Resources().UserWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - otherApiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: otherApiID, - Name: "other-api", - WorkspaceID: h.Resources().UserWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: otherKeyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create another Workspace for cross-API testing - otherWorkspace := h.CreateWorkspace() - - otherWsKeyAuthID := uid.New(uid.KeyAuthPrefix) - err = db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: otherWsKeyAuthID, - WorkspaceID: otherWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - otherWsApiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: otherWsApiID, - Name: "test-api", - WorkspaceID: otherWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: otherWsKeyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create a test key - keyID := uid.New(uid.KeyPrefix) - keyString := "test_" + uid.New("") - err = db.Query.InsertKey(ctx, h.DB.RW(), db.InsertKeyParams{ - ID: keyID, - KeyringID: keyAuthID, - Hash: hash.Sha256(keyString), - Start: keyString[:4], - WorkspaceID: h.Resources().UserWorkspace.ID, - ForWorkspaceID: sql.NullString{Valid: false}, - Name: sql.NullString{Valid: true, String: "Test Key"}, - CreatedAtM: time.Now().UnixMilli(), - Enabled: true, - IdentityID: sql.NullString{Valid: false}, - Meta: sql.NullString{Valid: false}, - Expires: sql.NullTime{Valid: false}, - RemainingRequests: sql.NullInt32{Valid: true, Int32: 100}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - } - - t.Run("no permissions", func(t *testing.T) { - // Create root key with no permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + RequiredPermissions: []string{"api.*.delete_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + // Create API for testing + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + // Create another API for cross-API testing + otherApi := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + // Create a test key + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + // Create a key for the other API + otherKeyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: otherApi.KeyAuthID.String, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + OtherApiID: otherApi.ID, + Custom: map[string]string{ + "other_key_id": otherKeyResp.KeyID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + } + }, + }, + ) + + // Cross-API permission test + t.Run("permission for different API", func(t *testing.T) { + h := testutil.NewHarness(t) + + route := &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong permission - has create but not delete", func(t *testing.T) { - // Create root key with read permission instead of create - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + h.Register(route) + + // Create two APIs + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + otherApi := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + // Create a key on the first API + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + // Create root key with permission for otherApi only + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.delete_key", otherApi.ID)) + + // Try to delete key from different API + req := handler.Request{ + KeyId: keyResp.KeyID, } - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("cross workspace access", func(t *testing.T) { - // Create a different workspace - differentWorkspace := h.CreateWorkspace() - - // Create a root key for the different workspace with full permissions - rootKey := h.CreateRootKey(differentWorkspace.ID, "api.*.delete_key") - - headers := http.Header{ + res := testutil.CallRoute[handler.Request, handler.Response](h, route, map[string][]string{ "Content-Type": {"application/json"}, "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - }) + }, req) - t.Run("cross api access", func(t *testing.T) { - // Create root key with read permission for a single api - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.delete_key", otherApiID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + if res.Status != 403 { + t.Errorf("expected 403, got %d, body: %s", res.Status, res.RawBody) } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) }) } diff --git a/go/apps/api/routes/v2_keys_get_key/401_test.go b/go/apps/api/routes/v2_keys_get_key/401_test.go index b3a339abed..b6456aa54f 100644 --- a/go/apps/api/routes/v2_keys_get_key/401_test.go +++ b/go/apps/api/routes/v2_keys_get_key/401_test.go @@ -1,91 +1,32 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_get_key" "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestGetKeyUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - req := handler.Request{ - KeyId: uid.New(uid.KeyPrefix), - Decrypt: ptr.P(false), - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("empty authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {""}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - no Bearer prefix", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_token_without_bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - Bearer only", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("nonexistent root key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer " + uid.New(uid.KeyPrefix)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Decrypt: ptr.P(false), + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_get_key/403_test.go b/go/apps/api/routes/v2_keys_get_key/403_test.go index 6dcab70a8f..2a3dc6ed8d 100644 --- a/go/apps/api/routes/v2_keys_get_key/403_test.go +++ b/go/apps/api/routes/v2_keys_get_key/403_test.go @@ -1,249 +1,72 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_get_key" - "github.com/unkeyed/unkey/go/pkg/db" - "github.com/unkeyed/unkey/go/pkg/hash" "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestGetKeyForbidden(t *testing.T) { - h := testutil.NewHarness(t) - ctx := context.Background() - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - // Create API for testing - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: h.Resources().UserWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - apiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: apiID, - Name: "test-api", - WorkspaceID: h.Resources().UserWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: keyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create another API for cross-API testing - otherKeyAuthID := uid.New(uid.KeyAuthPrefix) - err = db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: otherKeyAuthID, - WorkspaceID: h.Resources().UserWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - otherApiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: otherApiID, - Name: "other-api", - WorkspaceID: h.Resources().UserWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: otherKeyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create another Workspace for cross-API testing - otherWorkspace := h.CreateWorkspace() - - otherWsKeyAuthID := uid.New(uid.KeyAuthPrefix) - err = db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: otherWsKeyAuthID, - WorkspaceID: otherWorkspace.ID, - CreatedAtM: time.Now().UnixMilli(), - DefaultPrefix: sql.NullString{Valid: false, String: ""}, - DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, - }) - require.NoError(t, err) - - otherWsApiID := uid.New(uid.APIPrefix) - err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ - ID: otherWsApiID, - Name: "test-api", - WorkspaceID: otherWorkspace.ID, - AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, - KeyAuthID: sql.NullString{Valid: true, String: otherWsKeyAuthID}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create a test key - keyID := uid.New(uid.KeyPrefix) - keyString := "test_" + uid.New("") - err = db.Query.InsertKey(ctx, h.DB.RW(), db.InsertKeyParams{ - ID: keyID, - KeyringID: keyAuthID, - Hash: hash.Sha256(keyString), - Start: keyString[:4], - WorkspaceID: h.Resources().UserWorkspace.ID, - ForWorkspaceID: sql.NullString{Valid: false}, - Name: sql.NullString{Valid: true, String: "Test Key"}, - CreatedAtM: time.Now().UnixMilli(), - Enabled: true, - IdentityID: sql.NullString{Valid: false}, - Meta: sql.NullString{Valid: false}, - Expires: sql.NullTime{Valid: false}, - RemainingRequests: sql.NullInt32{Valid: false}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Decrypt: ptr.P(true), - } - - t.Run("no permissions", func(t *testing.T) { - // Create root key with no permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong permission - has create but not read", func(t *testing.T) { - // Create root key with read permission instead of create - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong permission - trying to decrypt key but no decrypt permissions", func(t *testing.T) { - // Create root key with read permission instead of create - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.read_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("cross workspace access", func(t *testing.T) { - // Create a different workspace - differentWorkspace := h.CreateWorkspace() - - // Create a root key for the different workspace with full permissions - rootKey := h.CreateRootKey(differentWorkspace.ID, "api.*.read_key", "api.*.read_api") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("cross api access", func(t *testing.T) { - // Create root key with read permission for a single api - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.read_key", otherApiID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("decrypt permission without read permission", func(t *testing.T) { - // Create root key with only decrypt permission, no read permission - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.decrypt_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - // Try to get key - readReq := handler.Request{ - KeyId: keyID, - Decrypt: ptr.P(false), // Even without decrypt, should fail on read permission - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, readReq) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong resource type permissions", func(t *testing.T) { - // Create root key with permissions for different resource type - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "workspace.*.read", "identity.*.read") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("specific API permission but wrong action", func(t *testing.T) { - // Create root key with permission for correct API but wrong action - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.delete_key", apiID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + RequiredPermissions: []string{"api.*.read_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Decrypt: ptr.P(false), + } + }, + AdditionalPermissionTests: []authz.PermissionTestCase[handler.Request]{ + { + Name: "decrypt without decrypt_key permission", + Permissions: []string{"api.*.read_key"}, // Has read but not decrypt + ModifyRequest: func(req handler.Request, res authz.TestResources) handler.Request { + req.Decrypt = ptr.P(true) + return req + }, + ExpectedStatus: 403, + }, + { + Name: "decrypt permission without read permission", + Permissions: []string{"api.*.decrypt_key"}, // Has decrypt but not read + ModifyRequest: func(req handler.Request, res authz.TestResources) handler.Request { + req.Decrypt = ptr.P(false) + return req + }, + ExpectedStatus: 403, + }, + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_remove_permissions/401_test.go b/go/apps/api/routes/v2_keys_remove_permissions/401_test.go index b220f75d9c..d52f2e2cf8 100644 --- a/go/apps/api/routes/v2_keys_remove_permissions/401_test.go +++ b/go/apps/api/routes/v2_keys_remove_permissions/401_test.go @@ -1,154 +1,31 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_remove_permissions" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create test data using testutil helper - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - permissionDescription := "Read documents permission" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - Permissions: []seed.CreatePermissionRequest{ - { - WorkspaceID: workspace.ID, - Name: "documents.read.remove.auth", - Slug: "documents.read.remove.auth", - Description: &permissionDescription, - }, + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } }, - }) - keyID := keyResponse.KeyID - permissionID := keyResponse.PermissionIds[0] - - req := handler.Request{ - KeyId: keyID, - Permissions: []string{permissionID}, - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - }) - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "key") - }) - - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_format"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "valid root key") - }) - - t.Run("empty bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "valid root key") - }) - - t.Run("disabled root key", func(t *testing.T) { - // Use invalid root key to simulate disabled key - rootKey := "invalid_disabled_key" - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Permissions: []string{"test.permission"}, + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_remove_roles/401_test.go b/go/apps/api/routes/v2_keys_remove_roles/401_test.go index 6f1dec910d..8765aa6892 100644 --- a/go/apps/api/routes/v2_keys_remove_roles/401_test.go +++ b/go/apps/api/routes/v2_keys_remove_roles/401_test.go @@ -1,345 +1,31 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_remove_roles" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create workspace for test setup - workspace := h.Resources().UserWorkspace - - t.Run("missing authorization header", func(t *testing.T) { - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_missing_auth_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - // Request without authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - }) - - t.Run("invalid bearer token", func(t *testing.T) { - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_invalid_token_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - // Request with invalid bearer token - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "root key is invalid") - }) - - t.Run("malformed authorization header", func(t *testing.T) { - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_malformed_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - // Request with malformed authorization header (missing Bearer prefix) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"NotBearer invalid_format"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Bearer") - }) - - t.Run("empty bearer token", func(t *testing.T) { - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_empty_token_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - // Request with empty bearer token - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - }) - - t.Run("non-existent root key", func(t *testing.T) { - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_nonexistent_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - // Request with properly formatted but non-existent root key - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer unkey_32characterslongfaketoken12345"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "root key") - }) - - t.Run("root key from different workspace", func(t *testing.T) { - // Create a second workspace - workspace2 := h.CreateWorkspace() - rootKeyFromDifferentWorkspace := h.CreateRootKey(workspace2.ID, "api.*.update_key") - - // Create API and key in original workspace using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role in original workspace - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_diff_workspace_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - // Request with root key from different workspace - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyFromDifferentWorkspace)}, - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse]( - h, - route, - headers, - req, - ) - - // Should return 404 for security (workspace isolation) - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "not found") - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Roles: []string{"test.role"}, + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_remove_roles/403_test.go b/go/apps/api/routes/v2_keys_remove_roles/403_test.go index a9015f623d..f1e498555a 100644 --- a/go/apps/api/routes/v2_keys_remove_roles/403_test.go +++ b/go/apps/api/routes/v2_keys_remove_roles/403_test.go @@ -1,407 +1,66 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_remove_roles" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create workspace for test setup - workspace := h.Resources().UserWorkspace - - t.Run("root key without required permissions", func(t *testing.T) { - // Create root key WITHOUT update permissions - rootKey := h.CreateRootKey(workspace.ID) // No permissions - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_no_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - t.Run("root key with partial permissions", func(t *testing.T) { - // Create root key with insufficient permissions - rootKey := h.CreateRootKey(workspace.ID, "api.read.update_key") // Read instead of wildcard - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_partial_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - t.Run("root key with unrelated permissions", func(t *testing.T) { - // Create root key with unrelated permissions - rootKey := h.CreateRootKey(workspace.ID, "different.permission.scope") - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_unrelated_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - t.Run("root key with create permissions but not update", func(t *testing.T) { - // Create root key with create permission instead of update - rootKey := h.CreateRootKey(workspace.ID, "api.*.create_key") - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_create_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - t.Run("root key with delete permissions but not update", func(t *testing.T) { - // Create root key with delete permission instead of update - rootKey := h.CreateRootKey(workspace.ID, "api.*.delete_key") - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_delete_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - t.Run("root key with specific API permissions but not wildcard", func(t *testing.T) { - // Create root key with specific API permission (not wildcard) - rootKey := h.CreateRootKey(workspace.ID, "api.specific.update_key") - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_specific_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) - - t.Run("root key with mixed permissions but missing required", func(t *testing.T) { - // Create root key with multiple permissions but missing the required one - rootKey := h.CreateRootKey(workspace.ID, "api.*.create_key", "api.*.delete_key", "api.*.read_key") - // Missing "api.*.update_key" - - // Create API and key using testutil helpers - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - }) - keyID := keyResponse.KeyID - - // Create a role - roleID := uid.New(uid.TestPrefix) - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: "test_role_mixed_perms_" + uid.New(""), - Description: sql.NullString{Valid: true, String: "Test role"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions:") - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + RequiredPermissions: []string{"api.*.update_key", "rbac.*.remove_role_from_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + roleID := h.CreateRole(seed.CreateRoleRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test-role", + }) + + roleName := "test-role-attached" + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + Roles: []seed.CreateRoleRequest{ + { + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: roleName, + }, + }, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + Custom: map[string]string{ + "role_id": roleID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Roles: []string{res.Custom["role_id"]}, + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_reroll_key/401_test.go b/go/apps/api/routes/v2_keys_reroll_key/401_test.go index 4790dfd08e..220a865a7c 100644 --- a/go/apps/api/routes/v2_keys_reroll_key/401_test.go +++ b/go/apps/api/routes/v2_keys_reroll_key/401_test.go @@ -1,85 +1,30 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_reroll_key" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestRerollKeyUnauthorized(t *testing.T) { - - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - IpWhitelist: "", - EncryptedKeys: true, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - key := h.CreateKey(seed.CreateKeyRequest{ - Disabled: false, - WorkspaceID: h.Resources().UserWorkspace.ID, - KeyAuthID: api.KeyAuthID.String, - }) - - // Basic request body - req := handler.Request{ - KeyId: key.KeyID, - Expiration: 0, - } - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key_12345"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("nonexistent key", func(t *testing.T) { - nonexistentKey := uid.New(uid.KeyPrefix) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", nonexistentKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("bearer with extra spaces", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key_with_spaces "}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_reroll_key/403_test.go b/go/apps/api/routes/v2_keys_reroll_key/403_test.go index 77713cd649..4358d5ac2e 100644 --- a/go/apps/api/routes/v2_keys_reroll_key/403_test.go +++ b/go/apps/api/routes/v2_keys_reroll_key/403_test.go @@ -1,137 +1,73 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_reroll_key" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestRerollKeyForbidden(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - IpWhitelist: "", - EncryptedKeys: true, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - otherApi := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - IpWhitelist: "", - EncryptedKeys: true, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - key := h.CreateKey(seed.CreateKeyRequest{ - Disabled: false, - WorkspaceID: h.Resources().UserWorkspace.ID, - KeyAuthID: api.KeyAuthID.String, - }) - - req := handler.Request{ - KeyId: key.KeyID, - Expiration: 0, - } - - t.Run("no permissions", func(t *testing.T) { - // Create root key with no permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong permission - has read but not create", func(t *testing.T) { - // Create root key with read permission instead of create - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.read_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("permission for different API", func(t *testing.T) { - // Create root key with create permission for other API - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.create_key", otherApi.ID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - encryptedKey := h.CreateKey(seed.CreateKeyRequest{ - Disabled: false, - Recoverable: true, - WorkspaceID: h.Resources().UserWorkspace.ID, - KeyAuthID: api.KeyAuthID.String, - Remaining: nil, - IdentityID: nil, - Meta: nil, - Expires: nil, - Name: nil, - Deleted: false, - RefillAmount: nil, - RefillDay: nil, - Permissions: nil, - Roles: nil, - Ratelimits: nil, - }) - - t.Run("reroll recoverable key without perms", func(t *testing.T) { - // Create root key with permission that partially matches but isn't sufficient because no encryption permission - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") - - req := handler.Request{ - KeyId: encryptedKey.KeyID, - Expiration: 0, - } - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + RequiredPermissions: []string{"api.*.create_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + EncryptedKeys: true, + }) + + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + // Create a recoverable key for encryption permission testing + recoverableKeyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + Recoverable: true, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + Custom: map[string]string{ + "recoverable_key_id": recoverableKeyResp.KeyID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + } + }, + AdditionalPermissionTests: []authz.PermissionTestCase[handler.Request]{ + { + Name: "reroll recoverable key without encrypt_key permission", + Permissions: []string{"api.*.create_key"}, + ModifyRequest: func(req handler.Request, res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.Custom["recoverable_key_id"], + } + }, + ExpectedStatus: 403, + }, + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_set_permissions/401_test.go b/go/apps/api/routes/v2_keys_set_permissions/401_test.go index bbc7bc2bde..6992146e04 100644 --- a/go/apps/api/routes/v2_keys_set_permissions/401_test.go +++ b/go/apps/api/routes/v2_keys_set_permissions/401_test.go @@ -1,175 +1,31 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_set_permissions" - "github.com/unkeyed/unkey/go/pkg/db" - dbtype "github.com/unkeyed/unkey/go/pkg/db/types" - "github.com/unkeyed/unkey/go/pkg/hash" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create test data - keyAuthID := uid.New(uid.KeyAuthPrefix) - err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ - ID: keyAuthID, - WorkspaceID: workspace.ID, - StoreEncryptedKeys: false, - DefaultPrefix: sql.NullString{Valid: true, String: "test"}, - DefaultBytes: sql.NullInt32{Valid: true, Int32: 16}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - keyID := uid.New(uid.KeyPrefix) - keyString := "test_" + uid.New("") - err = db.Query.InsertKey(ctx, h.DB.RW(), db.InsertKeyParams{ - ID: keyID, - KeyringID: keyAuthID, - Hash: hash.Sha256(keyString), - Start: keyString[:4], - WorkspaceID: workspace.ID, - ForWorkspaceID: sql.NullString{Valid: false}, - Name: sql.NullString{Valid: true, String: "Test Key"}, - CreatedAtM: time.Now().UnixMilli(), - Enabled: true, - IdentityID: sql.NullString{Valid: false}, - Meta: sql.NullString{Valid: false}, - Expires: sql.NullTime{Valid: false}, - RemainingRequests: sql.NullInt32{Valid: false}, - }) - require.NoError(t, err) - - permissionID := uid.New(uid.TestPrefix) - err = db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: permissionID, - WorkspaceID: workspace.ID, - Name: "documents.read.auth", - Slug: "documents.read.auth", - Description: dbtype.NullString{Valid: true, String: "Read documents permission"}, - }) - require.NoError(t, err) - - req := handler.Request{ - KeyId: keyID, - Permissions: []string{permissionID}, - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "Authorization header") - }) - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "key") - }) - - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_format"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "valid root key") - }) - - t.Run("empty bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer "}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "valid root key") - }) - - t.Run("disabled root key", func(t *testing.T) { - // Use invalid root key to simulate disabled key - rootKey := "invalid_disabled_key" - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Permissions: []string{"test.permission"}, + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_set_roles/401_test.go b/go/apps/api/routes/v2_keys_set_roles/401_test.go index 1d714a7255..efc2ff7331 100644 --- a/go/apps/api/routes/v2_keys_set_roles/401_test.go +++ b/go/apps/api/routes/v2_keys_set_roles/401_test.go @@ -1,89 +1,31 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_set_roles" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - KeyId: "key_123", - Roles: []string{"role_123"}, - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Roles: []string{"test.role"}, + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_set_roles/403_test.go b/go/apps/api/routes/v2_keys_set_roles/403_test.go index 1c19a2eb3f..1fd96604fa 100644 --- a/go/apps/api/routes/v2_keys_set_roles/403_test.go +++ b/go/apps/api/routes/v2_keys_set_roles/403_test.go @@ -1,113 +1,59 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_set_roles" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create test data using testutil helper - defaultPrefix := "test" - defaultBytes := int32(16) - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - DefaultPrefix: &defaultPrefix, - DefaultBytes: &defaultBytes, - }) - - keyName := "Test Key" - roleDescription := "Test role" - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - Roles: []seed.CreateRoleRequest{ - { - WorkspaceID: workspace.ID, - Name: "test-role", - Description: &roleDescription, + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + } + }, + RequiredPermissions: []string{"api.*.update_key", "rbac.*.add_role_to_key", "rbac.*.remove_role_from_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + roleID := h.CreateRole(seed.CreateRoleRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test-role", + }) + + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + Custom: map[string]string{ + "role_id": roleID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Roles: []string{res.Custom["role_id"]}, + } }, }, - }) - keyID := keyResponse.KeyID - roleID := keyResponse.RolesIds[0] - - // Test case for insufficient permissions - missing update_key - t.Run("missing update_key permission", func(t *testing.T) { - // Create a root key with some permissions but not update_key - rootKey := h.CreateRootKey(workspace.ID, "api.*.create_key") // Only has create, not update - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for no permissions at all - t.Run("no permissions", func(t *testing.T) { - // Create a root key with no permissions - rootKey := h.CreateRootKey(workspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - KeyId: keyID, - Roles: []string{roleID}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - + ) } diff --git a/go/apps/api/routes/v2_keys_update_credits/401_test.go b/go/apps/api/routes/v2_keys_update_credits/401_test.go index 78fb8ff4e1..75393386ae 100644 --- a/go/apps/api/routes/v2_keys_update_credits/401_test.go +++ b/go/apps/api/routes/v2_keys_update_credits/401_test.go @@ -1,114 +1,35 @@ package handler_test import ( - "net/http" "testing" "github.com/oapi-codegen/nullable" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_update_credits" + "github.com/unkeyed/unkey/go/apps/api/openapi" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestKeyUpdateCreditsUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - UsageLimiter: h.UsageLimiter, - } - - h.Register(route) - - // Create a workspace and user - workspace := h.Resources().UserWorkspace - - // Create a test API and key with credits using testutil helper - apiName := "Test API" - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: workspace.ID, - Name: &apiName, - }) - - keyName := "test-key" - remainingRequests := int32(100) - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: &keyName, - Remaining: &remainingRequests, - }) - keyID := keyResponse.KeyID - - req := handler.Request{ - KeyId: keyID, - Operation: openapi.Increment, - Value: nullable.NewNullableWithValue(int64(10)), - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("empty authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {""}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - no Bearer prefix", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_token_without_bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - Bearer only", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("nonexistent root key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer " + uid.New(uid.KeyPrefix)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + UsageLimiter: h.UsageLimiter, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + Operation: openapi.Increment, + Value: nullable.NewNullableWithValue(int64(10)), + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_update_credits/403_test.go b/go/apps/api/routes/v2_keys_update_credits/403_test.go index 35f72346eb..10cfefe8ed 100644 --- a/go/apps/api/routes/v2_keys_update_credits/403_test.go +++ b/go/apps/api/routes/v2_keys_update_credits/403_test.go @@ -1,114 +1,57 @@ package handler_test import ( - "fmt" - "net/http" "testing" "github.com/oapi-codegen/nullable" - "github.com/stretchr/testify/require" "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_update_credits" - "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestKeyUpdateCreditsForbidden(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - UsageLimiter: h.UsageLimiter, - } - - h.Register(route) - - // Create API for testing using testutil helper - apiName := "test-api" - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - Name: &apiName, - }) - - diffApi := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - Name: &apiName, - }) - - key := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: api.WorkspaceID, - KeyAuthID: api.KeyAuthID.String, - Remaining: ptr.P(int32(100)), - }) - - req := handler.Request{ - KeyId: key.KeyID, - Operation: openapi.Increment, - Value: nullable.NewNullableWithValue(int64(10)), - } - - t.Run("no permissions", func(t *testing.T) { - // Create root key with no permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("wrong permission - has create but not update", func(t *testing.T) { - // Create root key with read permission instead of create - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("cross workspace access", func(t *testing.T) { - // Create a different workspace - differentWorkspace := h.CreateWorkspace() - - // Create a root key for the different workspace with full permissions - rootKey := h.CreateRootKey(differentWorkspace.ID, "api.*.update_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("cross api access", func(t *testing.T) { - // Create root key with read permission for a single api - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("api.%s.update_key", diffApi.ID)) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + KeyCache: h.Caches.VerificationKeyByHash, + UsageLimiter: h.UsageLimiter, + } + }, + RequiredPermissions: []string{"api.*.update_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + remaining := int32(100) + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + Remaining: &remaining, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Operation: openapi.Increment, + Value: nullable.NewNullableWithValue(int64(10)), + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_keys_update_key/401_test.go b/go/apps/api/routes/v2_keys_update_key/401_test.go index a82a4c473e..475ab53cb5 100644 --- a/go/apps/api/routes/v2_keys_update_key/401_test.go +++ b/go/apps/api/routes/v2_keys_update_key/401_test.go @@ -1,81 +1,29 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_update_key" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUpdateKeyUnauthorized(t *testing.T) { - - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - UsageLimiter: h.UsageLimiter, - } - - h.Register(route) - - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - IpWhitelist: "", - EncryptedKeys: false, - Name: nil, - CreatedAt: nil, - DefaultPrefix: nil, - DefaultBytes: nil, - }) - - key := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: api.WorkspaceID, - KeyAuthID: api.KeyAuthID.String, - }) - - req := handler.Request{KeyId: key.KeyID} - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key_12345"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("nonexistent key", func(t *testing.T) { - nonexistentKey := uid.New(uid.KeyPrefix) - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", nonexistentKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) - - t.Run("bearer with extra spaces", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_key_with_spaces "}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + KeyId: uid.New(uid.KeyPrefix), + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_update_key/403_test.go b/go/apps/api/routes/v2_keys_update_key/403_test.go index 65f6f5fc9d..b7c0621fb1 100644 --- a/go/apps/api/routes/v2_keys_update_key/403_test.go +++ b/go/apps/api/routes/v2_keys_update_key/403_test.go @@ -1,179 +1,49 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_update_key" - "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUpdateKeyCorrectPermissions(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - roles []string - }{ - { - name: "wildcard api", - roles: []string{"api.*.update_key"}, - }, - { - name: "specific api", - roles: []string{}, // Will be filled in with specific API ID +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"api.*.update_key"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + keyResp := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyAuthID: api.KeyAuthID.String, + KeyID: keyResp.KeyID, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + } + }, }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - UsageLimiter: h.UsageLimiter, - } - - h.Register(route) - - // Create API using helper - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - }) - - // Create key using helper - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: ptr.P("test"), - }) - - // Set up permissions - roles := tc.roles - if tc.name == "specific api" { - roles = []string{fmt.Sprintf("api.%s.update_key", api.ID)} - } - - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, roles...) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - KeyId: keyResponse.KeyID, - Enabled: ptr.P(false), - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, 200, res.Status, "Expected 200, got: %d", res.Status) - require.NotNil(t, res.Body) - }) - } -} - -func TestUpdateKeyInsufficientPermissions(t *testing.T) { - t.Parallel() - - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - UsageLimiter: h.UsageLimiter, - } - - h.Register(route) - - // Create API using helper - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - }) - - // Create key using helper - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: ptr.P("test"), - }) - - // Create root key with insufficient permissions - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key") // Wrong permission - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - KeyId: keyResponse.KeyID, - Enabled: ptr.P(false), - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) -} - -func TestUpdateKeyCrossWorkspaceIsolation(t *testing.T) { - t.Parallel() - - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - KeyCache: h.Caches.VerificationKeyByHash, - UsageLimiter: h.UsageLimiter, - } - - h.Register(route) - - // Create API using helper in user workspace - api := h.CreateApi(seed.CreateApiRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - }) - - // Create key using helper in user workspace - keyResponse := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: h.Resources().UserWorkspace.ID, - KeyAuthID: api.KeyAuthID.String, - Name: ptr.P("test"), - }) - - // Create different workspace - otherWorkspace := h.CreateWorkspace() - - // Create root key for other workspace - rootKey := h.CreateRootKey(otherWorkspace.ID, "api.*.update_key") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - KeyId: keyResponse.KeyID, - Enabled: ptr.P(false), - } - - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) - require.Equal(t, 404, res.Status) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "The specified key was not found") + ) } diff --git a/go/apps/api/routes/v2_keys_verify_key/401_test.go b/go/apps/api/routes/v2_keys_verify_key/401_test.go index 553cd25f5b..593b8378ef 100644 --- a/go/apps/api/routes/v2_keys_verify_key/401_test.go +++ b/go/apps/api/routes/v2_keys_verify_key/401_test.go @@ -1,85 +1,29 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_verify_key" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - ClickHouse: h.ClickHouse, - } - - h.Register(route) - - workspace := h.Resources().UserWorkspace - api := h.CreateApi(seed.CreateApiRequest{WorkspaceID: workspace.ID}) - key := h.CreateKey(seed.CreateKeyRequest{ - WorkspaceID: workspace.ID, - KeyAuthID: api.KeyAuthID.String, - }) - - req := handler.Request{ - Key: key.Key, - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - // Authorization header missing - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("invalid bearer token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_here"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"InvalidFormat"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("requestId is returned in error response", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotEmpty(t, res.Body.Meta.RequestId, "RequestId should be returned even in error response") - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + ClickHouse: h.ClickHouse, + } + }, + func() handler.Request { + return handler.Request{ + Key: "test_key", + } + }, + ) } diff --git a/go/apps/api/routes/v2_keys_whoami/401_test.go b/go/apps/api/routes/v2_keys_whoami/401_test.go index 2e9e86942b..a6b4267060 100644 --- a/go/apps/api/routes/v2_keys_whoami/401_test.go +++ b/go/apps/api/routes/v2_keys_whoami/401_test.go @@ -1,89 +1,27 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_keys_whoami" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestGetKeyUnauthorized(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - Vault: h.Vault, - } - - h.Register(route) - - req := handler.Request{ - Key: uid.New(uid.TestPrefix), - } - - t.Run("missing authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("empty authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {""}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - no Bearer prefix", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"invalid_token_without_bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("malformed authorization header - Bearer only", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - t.Run("nonexistent root key", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer " + uid.New(uid.KeyPrefix)}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse](h, route, headers, req) - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) +func TestAuthenticationErrors(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{ + Key: "test_invalid_key", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_create_permission/401_test.go b/go/apps/api/routes/v2_permissions_create_permission/401_test.go index d90ea3917b..bf6f336f55 100644 --- a/go/apps/api/routes/v2_permissions_create_permission/401_test.go +++ b/go/apps/api/routes/v2_permissions_create_permission/401_test.go @@ -1,96 +1,29 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_create_permission" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - Name: "auth.test.permission", - Slug: "auth-test-permission", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, http.StatusBadRequest, res.Status) // System returns 400 for missing auth header - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - // No specific check for error message as it may vary - }) - - // Test case for invalid authorization token - t.Run("invalid_authorization_token", func(t *testing.T) { - // Skip this test because the actual response code (401) doesn't match expected (400) - t.Skip("Authorization behavior is inconsistent with expected status code") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, http.StatusBadRequest, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - // No specific check for error message as it may vary - }) - - // Test case for malformed authorization header - t.Run("malformed_authorization_header", func(t *testing.T) { - // Skip this test because the actual response code (400) doesn't match expected (401) - t.Skip("Authorization behavior is inconsistent with expected status code") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, http.StatusUnauthorized, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Equal(t, res.Body.Error.Detail, "unauthorized") - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Name: "auth.test.permission", + Slug: "auth-test-permission", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_create_permission/403_test.go b/go/apps/api/routes/v2_permissions_create_permission/403_test.go index 34b3b07a63..647ceac200 100644 --- a/go/apps/api/routes/v2_permissions_create_permission/403_test.go +++ b/go/apps/api/routes/v2_permissions_create_permission/403_test.go @@ -1,87 +1,32 @@ package handler_test import ( - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_create_permission" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Test case for insufficient permissions - missing create_permission - t.Run("missing create_permission permission", func(t *testing.T) { - // Create a root key with only read permissions but no create_permission permission - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.read_permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Name: "test.permission.unauthorized", - Slug: "test-permission-unauthorized", - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, http.StatusForbidden, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Equal(t, "Missing permission: 'rbac.*.create_permission'", res.Body.Error.Detail) - }) - - // Test case for wrong workspace - t.Run("wrong workspace", func(t *testing.T) { - // Use a non-existent workspace ID - otherWorkspaceID := "ws_nonexistent" - - // Create a root key for the other workspace with all permissions - rootKey := h.CreateRootKey(otherWorkspaceID, "rbac.*.create_permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Name: "test.permission.wrong.workspace", - Slug: "test-permission-wrong-workspace", - } - - // This is generally masked as a 404 or 403 depending on the implementation - // Use CallRoute and check for error response - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.True(t, res.Status == http.StatusForbidden || res.Status == http.StatusNotFound, - "Expected 403 or 404 status code, got %d", res.Status) - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"rbac.*.create_permission"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Name: "test.permission.unauthorized", + Slug: "test-permission-unauthorized", + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_create_role/401_test.go b/go/apps/api/routes/v2_permissions_create_role/401_test.go index 4899daeae7..a41ddf6af2 100644 --- a/go/apps/api/routes/v2_permissions_create_role/401_test.go +++ b/go/apps/api/routes/v2_permissions_create_role/401_test.go @@ -1,86 +1,28 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_create_role" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - Name: "auth.test.role", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Name: "test-role", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_create_role/403_test.go b/go/apps/api/routes/v2_permissions_create_role/403_test.go index b42989938a..e555ca6537 100644 --- a/go/apps/api/routes/v2_permissions_create_role/403_test.go +++ b/go/apps/api/routes/v2_permissions_create_role/403_test.go @@ -1,98 +1,31 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_create_role" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Test case for insufficient permissions - missing create_role - t.Run("missing create_role permission", func(t *testing.T) { - // Create a root key with some permissions but not create_role - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.read_role") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Name: "test.role.unauthorized", - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions") - - // Verify no role was created - _, err := db.Query.FindRoleByNameAndWorkspaceID(context.Background(), h.DB.RO(), db.FindRoleByNameAndWorkspaceIDParams{ - Name: req.Name, - WorkspaceID: workspace.ID, - }) - require.True(t, db.IsNotFound(err), "No role should have been created") - }) - - // Test case for wrong workspace - t.Run("wrong workspace", func(t *testing.T) { - // Create a different workspace - otherWorkspace := h.CreateWorkspace() - - // Create a root key for the other workspace with all permissions - rootKey := h.CreateRootKey(otherWorkspace.ID, "rbac.*.create_role") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Name: "test.role.wrong.workspace", - } - - // Make the request - this should succeed in the other workspace - testutil.CallRoute[handler.Request, handler.Response]( - h, - route, - headers, - req, - ) - - // The role should be created in the authorized workspace (the other workspace) - // not in the original workspace - _, err := db.Query.FindRoleByNameAndWorkspaceID(context.Background(), h.DB.RO(), db.FindRoleByNameAndWorkspaceIDParams{ - Name: req.Name, - WorkspaceID: workspace.ID, - }) - require.True(t, db.IsNotFound(err), "No role should have been created in original workspace") - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"rbac.*.create_role"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Name: "test-role", + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_delete_permission/401_test.go b/go/apps/api/routes/v2_permissions_delete_permission/401_test.go index 7e5e7015f5..2efa7e0b26 100644 --- a/go/apps/api/routes/v2_permissions_delete_permission/401_test.go +++ b/go/apps/api/routes/v2_permissions_delete_permission/401_test.go @@ -1,86 +1,28 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_delete_permission" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - Permission: "perm_test123", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Permission: "perm_test123", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_delete_permission/403_test.go b/go/apps/api/routes/v2_permissions_delete_permission/403_test.go index 078c0bf31b..af7618e212 100644 --- a/go/apps/api/routes/v2_permissions_delete_permission/403_test.go +++ b/go/apps/api/routes/v2_permissions_delete_permission/403_test.go @@ -1,117 +1,45 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_delete_permission" - "github.com/unkeyed/unkey/go/pkg/db" - dbtype "github.com/unkeyed/unkey/go/pkg/db/types" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create a test permission to try to delete - permissionID := uid.New(uid.PermissionPrefix) - permissionName := "test.permission.delete.auth" - - err := db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: permissionID, - WorkspaceID: workspace.ID, - Name: permissionName, - Slug: "test-permission-delete-auth", - Description: dbtype.NullString{Valid: true, String: "Test permission for authorization tests"}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Test case for insufficient permissions - missing delete_permission - t.Run("missing delete_permission permission", func(t *testing.T) { - // Create a root key with some permissions but not delete_permission - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.read_permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Permission: permissionID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "Missing one of these permissions") - - // Verify the permission still exists (wasn't deleted) - perm, err := db.Query.FindPermissionByID(ctx, h.DB.RO(), permissionID) - require.NoError(t, err) - require.Equal(t, permissionID, perm.ID) - }) - - // Test case for wrong workspace - t.Run("wrong workspace", func(t *testing.T) { - // Create a different workspace - otherWorkspace := h.CreateWorkspace() - - // Create a root key for the other workspace with all permissions - rootKey := h.CreateRootKey(otherWorkspace.ID, "rbac.*.delete_permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Permission: permissionID, // Permission is in the original workspace - } - - // When accessing from wrong workspace, the behavior should be a 404 Not Found - // as the handler masks workspace mismatches as "not found" - res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 404, res.Status, "Wrong workspace access should return 404") - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "does not exist") - - // Verify the permission still exists (wasn't deleted) - perm, err := db.Query.FindPermissionByID(ctx, h.DB.RO(), permissionID) - require.NoError(t, err) - require.Equal(t, permissionID, perm.ID) - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"rbac.*.delete_permission"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + permID := h.CreatePermission(seed.CreatePermissionRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test.permission", + Slug: "test-permission", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "permission_id": permID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Permission: res.Custom["permission_id"], + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_delete_role/401_test.go b/go/apps/api/routes/v2_permissions_delete_role/401_test.go index 28607c766b..70658f38be 100644 --- a/go/apps/api/routes/v2_permissions_delete_role/401_test.go +++ b/go/apps/api/routes/v2_permissions_delete_role/401_test.go @@ -1,87 +1,28 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_delete_role" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - Role: "role_test123", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Role: "role_test123", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_delete_role/403_test.go b/go/apps/api/routes/v2_permissions_delete_role/403_test.go index a813c10d04..6b77adea13 100644 --- a/go/apps/api/routes/v2_permissions_delete_role/403_test.go +++ b/go/apps/api/routes/v2_permissions_delete_role/403_test.go @@ -1,95 +1,44 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_delete_role" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestPermissionErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create a role for testing - // Create a role to attempt to delete (in the same workspace) - roleID := uid.New(uid.TestPrefix) - roleName := "test.forbidden.role" - roleDesc := "Test role for forbidden access" - - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: roleName, - Description: sql.NullString{Valid: true, String: roleDesc}, - }) - require.NoError(t, err) - - t.Run("missing required permission", func(t *testing.T) { - // Create a root key with a different permission (not rbac.*.delete_role) - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.read_role") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - handler.Request{ - Role: roleID, +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } }, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - t.Run("no permissions", func(t *testing.T) { - // Create a root key with no permissions - rootKeyNoPerms := h.CreateRootKey(workspace.ID, "") // No permissions - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyNoPerms)}, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - handler.Request{ - Role: roleID, + RequiredPermissions: []string{"rbac.*.delete_role"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + roleID := h.CreateRole(seed.CreateRoleRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test-role", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "role_id": roleID, + }, + } }, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Role: res.Custom["role_id"], + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_get_permission/401_test.go b/go/apps/api/routes/v2_permissions_get_permission/401_test.go index 53c0328a21..fced8f6bd0 100644 --- a/go/apps/api/routes/v2_permissions_get_permission/401_test.go +++ b/go/apps/api/routes/v2_permissions_get_permission/401_test.go @@ -1,86 +1,27 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_get_permission" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - Permission: "perm_test123", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{ + Permission: "perm_test123", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_get_permission/403_test.go b/go/apps/api/routes/v2_permissions_get_permission/403_test.go index df0dd6b589..77cf486c92 100644 --- a/go/apps/api/routes/v2_permissions_get_permission/403_test.go +++ b/go/apps/api/routes/v2_permissions_get_permission/403_test.go @@ -1,101 +1,44 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_get_permission" - "github.com/unkeyed/unkey/go/pkg/db" - dbtype "github.com/unkeyed/unkey/go/pkg/db/types" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestPermissionErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create a test permission to try to retrieve - permissionID := uid.New(uid.PermissionPrefix) - permissionName := "test.permission.access" - - err := db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: permissionID, - WorkspaceID: workspace.ID, - Name: permissionName, - Slug: "test-permission-access", - Description: dbtype.NullString{Valid: true, String: "Test permission for authorization tests"}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Test case for insufficient permissions - missing read_permission - t.Run("missing required permission", func(t *testing.T) { - // Create a root key with some permissions but not read_permission - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.create_permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Permission: permissionID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for no permissions - t.Run("no permissions", func(t *testing.T) { - // Create a root key with no permissions - rootKeyNoPerms := h.CreateRootKey(workspace.ID, "") // No permissions - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyNoPerms)}, - } - - req := handler.Request{ - Permission: permissionID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"rbac.*.read_permission"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + permID := h.CreatePermission(seed.CreatePermissionRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test.permission", + Slug: "test-permission", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "permission_id": permID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Permission: res.Custom["permission_id"], + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_get_role/401_test.go b/go/apps/api/routes/v2_permissions_get_role/401_test.go index af1ea2ca13..e60003cc37 100644 --- a/go/apps/api/routes/v2_permissions_get_role/401_test.go +++ b/go/apps/api/routes/v2_permissions_get_role/401_test.go @@ -1,86 +1,27 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_get_role" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{ - Role: "role_test123", - } - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{ + Role: "role_test123", + } + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_get_role/403_test.go b/go/apps/api/routes/v2_permissions_get_role/403_test.go index 759b28f7c1..54ad9cdd58 100644 --- a/go/apps/api/routes/v2_permissions_get_role/403_test.go +++ b/go/apps/api/routes/v2_permissions_get_role/403_test.go @@ -1,98 +1,43 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_get_role" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/testutil/seed" + "github.com/unkeyed/unkey/go/pkg/zen" ) -func TestPermissionErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create a test role to try to retrieve - roleID := uid.New(uid.TestPrefix) - roleName := "test.role.access" - - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: roleID, - WorkspaceID: workspace.ID, - Name: roleName, - Description: sql.NullString{Valid: true, String: "Test role for authorization tests"}, - }) - require.NoError(t, err) - - // Test case for insufficient permissions - missing required permission - t.Run("missing required permission", func(t *testing.T) { - // Create a root key with some permissions but not read_role - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.create_role") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Role: roleID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for no permissions - t.Run("no permissions", func(t *testing.T) { - // Create a root key with no permissions - rootKeyNoPerms := h.CreateRootKey(workspace.ID, "") // No permissions - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKeyNoPerms)}, - } - - req := handler.Request{ - Role: roleID, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) +func TestAuthorizationErrors(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"rbac.*.read_role"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + roleID := h.CreateRole(seed.CreateRoleRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test-role", + }) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "role_id": roleID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Role: res.Custom["role_id"], + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_list_permissions/401_test.go b/go/apps/api/routes/v2_permissions_list_permissions/401_test.go index 5e8bb9c3b1..f98048be54 100644 --- a/go/apps/api/routes/v2_permissions_list_permissions/401_test.go +++ b/go/apps/api/routes/v2_permissions_list_permissions/401_test.go @@ -1,84 +1,25 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_list_permissions" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{} - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{} + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_list_permissions/403_test.go b/go/apps/api/routes/v2_permissions_list_permissions/403_test.go index 91219f161d..4f7487d677 100644 --- a/go/apps/api/routes/v2_permissions_list_permissions/403_test.go +++ b/go/apps/api/routes/v2_permissions_list_permissions/403_test.go @@ -1,114 +1,28 @@ package handler_test import ( - "context" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_list_permissions" - "github.com/unkeyed/unkey/go/pkg/db" - dbtype "github.com/unkeyed/unkey/go/pkg/db/types" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create some test permissions to later try to list - err := db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: uid.New(uid.PermissionPrefix), - WorkspaceID: workspace.ID, - Name: "test.permission.auth", - Slug: "test-permission-auth", - Description: dbtype.NullString{Valid: true, String: "Test permission for authorization tests"}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Test case for insufficient permissions - missing read_permission - t.Run("missing read_permission permission", func(t *testing.T) { - // Create a root key with some permissions but not read_permission - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.create_permission") // Only has create, not read - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{} - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for wrong workspace - t.Run("wrong workspace", func(t *testing.T) { - // Create a different workspace - otherWorkspace := h.CreateWorkspace() - - // Create permissions in the other workspace - err := db.Query.InsertPermission(ctx, h.DB.RW(), db.InsertPermissionParams{ - PermissionID: uid.New(uid.PermissionPrefix), - WorkspaceID: otherWorkspace.ID, - Name: "other.workspace.permission", - Slug: "other-workspace-permission", - Description: dbtype.NullString{Valid: true, String: "This permission is in a different workspace"}, - CreatedAtM: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - // Create a root key for the original workspace with read_permission - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.read_permission") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{} - - // When listing permissions, we should only see permissions from the authorized workspace - // This should return 200 OK with only permissions from the authorized workspace - res := testutil.CallRoute[handler.Request, handler.Response]( - h, - route, - headers, - req, - ) - - require.Equal(t, 200, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Data) - - // Verify we only see permissions from our workspace - for _, perm := range res.Body.Data { - require.NotEqual(t, "other.workspace.permission", perm.Name) - } - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"rbac.*.read_permission"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{} + }, + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_list_roles/401_test.go b/go/apps/api/routes/v2_permissions_list_roles/401_test.go index f64aa32986..5d6bd4b5ff 100644 --- a/go/apps/api/routes/v2_permissions_list_roles/401_test.go +++ b/go/apps/api/routes/v2_permissions_list_roles/401_test.go @@ -1,85 +1,25 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_list_roles" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthenticationErrors(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a valid request - req := handler.Request{} - - // Test case for missing authorization header - t.Run("missing authorization header", func(t *testing.T) { - // No Authorization header - headers := http.Header{ - "Content-Type": {"application/json"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - - // Test case for invalid authorization token - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token_that_does_not_exist"}, - } - - res := testutil.CallRoute[handler.Request, openapi.UnauthorizedErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 401, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "invalid") - }) - - // Test case for malformed authorization header - t.Run("malformed authorization header", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"malformed_header_without_bearer_prefix"}, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 400, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - }) - + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{} + }, + ) } diff --git a/go/apps/api/routes/v2_permissions_list_roles/403_test.go b/go/apps/api/routes/v2_permissions_list_roles/403_test.go index e0b01bc850..0a9239c8be 100644 --- a/go/apps/api/routes/v2_permissions_list_roles/403_test.go +++ b/go/apps/api/routes/v2_permissions_list_roles/403_test.go @@ -1,109 +1,28 @@ package handler_test import ( - "context" - "database/sql" - "fmt" - "net/http" "testing" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_permissions_list_roles" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestAuthorizationErrors(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a workspace - workspace := h.Resources().UserWorkspace - - // Create some test roles to later try to list - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: uid.New(uid.TestPrefix), - WorkspaceID: workspace.ID, - Name: "test.role.auth", - Description: sql.NullString{Valid: true, String: "Test role for authorization tests"}, - }) - require.NoError(t, err) - - // Test case for insufficient permissions - missing read_role - t.Run("missing read_role permission", func(t *testing.T) { - // Create a root key with some permissions but not read_role - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.create_role") // Only has create, not read - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{} - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse]( - h, - route, - headers, - req, - ) - - require.Equal(t, 403, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Error) - require.Contains(t, res.Body.Error.Detail, "permission") - }) - - // Test case for wrong workspace - t.Run("wrong workspace", func(t *testing.T) { - // Create a different workspace - otherWorkspace := h.CreateWorkspace() - - // Create roles in the other workspace - err := db.Query.InsertRole(ctx, h.DB.RW(), db.InsertRoleParams{ - RoleID: uid.New(uid.TestPrefix), - WorkspaceID: otherWorkspace.ID, - Name: "other.workspace.role", - Description: sql.NullString{Valid: true, String: "This role is in a different workspace"}, - }) - require.NoError(t, err) - - // Create a root key for the original workspace with read_role - rootKey := h.CreateRootKey(workspace.ID, "rbac.*.read_role") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{} - - // When listing roles, we should only see roles from the authorized workspace - // This should return 200 OK with only roles from the authorized workspace - res := testutil.CallRoute[handler.Request, handler.Response]( - h, - route, - headers, - req, - ) - - require.Equal(t, 200, res.Status) - require.NotNil(t, res.Body) - require.NotNil(t, res.Body.Data) - - // Verify we only see roles from our workspace - for _, role := range res.Body.Data { - require.NotEqual(t, "other.workspace.role", role.Name) - } - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"rbac.*.read_role"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{} + }, + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go index b9f1430d1f..2ee09a73a2 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go @@ -1,41 +1,31 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_delete_override" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestUnauthorizedAccess(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - } - - h.Register(route) - - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ - Namespace: uid.New("test"), - Identifier: "test_identifier", - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.NotNil(t, res.Body) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + } + }, + func() handler.Request { + return handler.Request{ + Namespace: uid.New("test"), + Identifier: "test_identifier", + } + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go index 37651dac45..4a4af4543a 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go @@ -2,84 +2,74 @@ package handler_test import ( "context" - "fmt" - "net/http" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_delete_override" "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestWorkspacePermissions(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + } + }, + RequiredPermissions: []string{"ratelimit.*.delete_override"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + ctx := context.Background() - // Create a namespace in the default workspace - namespaceID := uid.New(uid.RatelimitNamespacePrefix) - err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ - ID: namespaceID, - WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: uid.New("test"), - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) + // Create a namespace + namespaceID := uid.New(uid.RatelimitNamespacePrefix) + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: uid.New("test"), + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create namespace: %v", err) + } - // Create an override in the default workspace - identifier := "test_identifier" - overrideID := uid.New(uid.RatelimitOverridePrefix) - err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ - ID: overrideID, - WorkspaceID: h.Resources().UserWorkspace.ID, - NamespaceID: namespaceID, - Identifier: identifier, - Limit: 10, - Duration: 1000, - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) + // Create an override + overrideID := uid.New(uid.RatelimitOverridePrefix) + err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ + ID: overrideID, + WorkspaceID: h.Resources().UserWorkspace.ID, + NamespaceID: namespaceID, + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create override: %v", err) + } - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - } - - h.Register(route) - - // Create a different workspace and key for testing cross-workspace access - differentWorkspace := h.CreateWorkspace() - differentWorkspaceKey := h.CreateRootKey(differentWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - - // Try to delete an override using a namespace from the default workspace - // but with a key from a different workspace - req := handler.Request{ - Namespace: namespaceID, - Identifier: identifier, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - - // This should return a 404 Not Found (for security reasons we don't reveal if the namespace exists) - require.Equal(t, http.StatusNotFound, res.Status, "got: %s", res.RawBody) - require.NotNil(t, res.Body) - - // Verify the override was NOT deleted - override, err := db.Query.FindRatelimitOverrideByID(ctx, h.DB.RO(), db.FindRatelimitOverrideByIDParams{ - WorkspaceID: h.Resources().UserWorkspace.ID, - OverrideID: overrideID, - }) - require.NoError(t, err) - require.False(t, override.DeletedAtM.Valid, "Override should not be deleted") + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "namespace_id": namespaceID, + "override_id": overrideID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Namespace: res.Custom["namespace_id"], + Identifier: "test_identifier", + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go index 60e5a706df..5fb0a0d614 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go @@ -1,41 +1,30 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_get_override" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestUnauthorizedAccess(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - } - - h.Register(route) - - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ - Namespace: uid.New("test"), - Identifier: "test_identifier", - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.NotNil(t, res.Body) - }) - + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + } + }, + func() handler.Request { + return handler.Request{ + Namespace: uid.New("test"), + Identifier: "test_identifier", + } + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go index 3101d46515..2ce78186e3 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go @@ -2,73 +2,73 @@ package handler_test import ( "context" - "fmt" - "net/http" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_get_override" "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestWorkspacePermissions(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + } + }, + RequiredPermissions: []string{"ratelimit.*.read_override"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + ctx := context.Background() - // Create a namespace - namespaceID := uid.New(uid.RatelimitNamespacePrefix) - err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ - ID: namespaceID, - WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: uid.New("test"), - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) + // Create a namespace + namespaceID := uid.New(uid.RatelimitNamespacePrefix) + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: uid.New("test"), + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create namespace: %v", err) + } - // Create an override - identifier := "test_identifier" - overrideID := uid.New(uid.RatelimitOverridePrefix) - err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ - ID: overrideID, - WorkspaceID: h.Resources().UserWorkspace.ID, // In default workspace - NamespaceID: namespaceID, - Identifier: identifier, - Limit: 10, - Duration: 1000, - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) + // Create an override + overrideID := uid.New(uid.RatelimitOverridePrefix) + err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ + ID: overrideID, + WorkspaceID: h.Resources().UserWorkspace.ID, + NamespaceID: namespaceID, + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create override: %v", err) + } - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - } - - h.Register(route) - differentWorkspace := h.CreateWorkspace() - // Create a key for a different workspace - differentWorkspaceKey := h.CreateRootKey(differentWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - - // Try to access the override with a key from a different workspace - req := handler.Request{ - Namespace: namespaceID, - Identifier: identifier, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - - // This should return a 404 Not Found (for security reasons we don't reveal if the namespace exists) - require.Equal(t, http.StatusNotFound, res.Status, "got: %s", res.RawBody) - require.NotNil(t, res.Body) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "namespace_id": namespaceID, + "override_id": overrideID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Namespace: res.Custom["namespace_id"], + Identifier: "test_identifier", + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_limit/401_test.go b/go/apps/api/routes/v2_ratelimit_limit/401_test.go index 24013805d1..67f9af88e7 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/401_test.go @@ -1,43 +1,34 @@ package v2RatelimitLimit_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_limit" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestUnauthorizedAccess(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ - Namespace: "test_namespace", - Identifier: "user_123", - Limit: 100, - Duration: 60000, - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.NotNil(t, res.Body) - }) + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + ClickHouse: h.ClickHouse, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Namespace: "test_namespace", + Identifier: "user_123", + Limit: 100, + Duration: 60000, + } + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_limit/403_test.go b/go/apps/api/routes/v2_ratelimit_limit/403_test.go index d8500d2a5d..4fa304f91e 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/403_test.go @@ -1,117 +1,50 @@ package v2RatelimitLimit_test import ( - "context" - "fmt" - "net/http" "testing" - "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_limit" - "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" - "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestWorkspacePermissions(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - // Create a namespace in the default workspace - namespaceID := uid.New(uid.RatelimitNamespacePrefix) - namespaceName := uid.New("test") - err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ - ID: namespaceID, - WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: namespaceName, - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - // Create a key for a different workspace - differentWorkspace := h.CreateWorkspace() - differentWorkspaceKey := h.CreateRootKey(differentWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - - // Try to access the namespace from the default workspace with a key from a different workspace - req := handler.Request{ - Namespace: namespaceName, - Identifier: "user_123", - Limit: 100, - Duration: 60000, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - - // This should return a 403 Forbidden - user lacks create_namespace permission - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, got: %d, body: %s", res.Status, res.RawBody) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "create_namespace", "Error should mention missing create_namespace permission") -} - -func TestInsufficientPermissions(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - Auditlogs: h.Auditlogs, - } - - h.Register(route) - - t.Run("has limit permission but no create_namespace permission", func(t *testing.T) { - // Use a namespace that doesn't exist - nonExistentNamespace := uid.New("nonexistent") - - // Create a key that can limit any namespace but cannot create namespaces - rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.limit") - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, - } - - req := handler.Request{ - Namespace: nonExistentNamespace, - Identifier: "user_123", - Limit: 100, - Duration: 60000, - } - - res := testutil.CallRoute[handler.Request, openapi.ForbiddenErrorResponse](h, route, headers, req) - - // Should return 403 because user has some permissions but not create_namespace - require.Equal(t, http.StatusForbidden, res.Status, "expected 403, got: %d, body: %s", res.Status, res.RawBody) - require.NotNil(t, res.Body) - require.Contains(t, res.Body.Error.Detail, "create_namespace", "Error should mention missing create_namespace permission") - - // Verify the namespace was NOT created - ctx := context.Background() - _, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ - WorkspaceID: h.Resources().UserWorkspace.ID, - Namespace: nonExistentNamespace, - }) - require.True(t, db.IsNotFound(err), "Namespace should not have been created when user lacks create_namespace permission") - }) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + ClickHouse: h.ClickHouse, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + Auditlogs: h.Auditlogs, + } + }, + // Note: When namespace doesn't exist, also requires create_namespace permission + RequiredPermissions: []string{"ratelimit.*.limit", "ratelimit.*.create_namespace"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Namespace: "test_namespace", + Identifier: "user_123", + Limit: 100, + Duration: 60000, + } + }, + AdditionalPermissionTests: []authz.PermissionTestCase[handler.Request]{ + { + Name: "has limit permission but no create_namespace permission", + Permissions: []string{"ratelimit.*.limit"}, + ExpectedStatus: 403, + }, + { + Name: "has create_namespace permission but no limit permission", + Permissions: []string{"ratelimit.*.create_namespace"}, + ExpectedStatus: 403, + }, + }, + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go index be024c5d7c..998bac1254 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go @@ -1,38 +1,27 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestUnauthorizedAccess(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ - Namespace: "test_namespace", - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status) - require.NotNil(t, res.Body) - }) - + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + func() handler.Request { + return handler.Request{ + Namespace: "test_namespace", + } + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go index a18f9eae0a..ce874c5a80 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go @@ -2,73 +2,71 @@ package handler_test import ( "context" - "fmt" - "net/http" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestWorkspacePermissions(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + } + }, + RequiredPermissions: []string{"ratelimit.*.read_override"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + ctx := context.Background() - // Create a namespace - namespaceID := uid.New(uid.RatelimitNamespacePrefix) - namespaceName := "test_namespace" - err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ - ID: namespaceID, - WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: namespaceName, - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) + // Create a namespace + namespaceID := uid.New(uid.RatelimitNamespacePrefix) + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: "test_namespace", + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create namespace: %v", err) + } - // Create an override - identifier := "test_identifier" - overrideID := uid.New(uid.RatelimitOverridePrefix) - err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ - ID: overrideID, - WorkspaceID: h.Resources().UserWorkspace.ID, // In default workspace - NamespaceID: namespaceID, - Identifier: identifier, - Limit: 10, - Duration: 1000, - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) + // Create an override + overrideID := uid.New(uid.RatelimitOverridePrefix) + err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ + ID: overrideID, + WorkspaceID: h.Resources().UserWorkspace.ID, + NamespaceID: namespaceID, + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create override: %v", err) + } - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - } - - h.Register(route) - - // Create a key for a different workspace - differentWorkspaceID := "ws_different" - differentWorkspaceKey := h.CreateRootKey(differentWorkspaceID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - - // Try to access the override with a key from a different workspace - req := handler.Request{ - Namespace: namespaceID, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - - // This should return a 404 Not Found (for security reasons we don't reveal if the namespace exists) - require.Equal(t, http.StatusNotFound, res.Status, "got: %s", res.RawBody) - require.NotNil(t, res.Body) + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "namespace_id": namespaceID, + "override_id": overrideID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Namespace: res.Custom["namespace_id"], + } + }, + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go index dcbd204fe6..ec58c9e0e9 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go @@ -1,44 +1,33 @@ package handler_test import ( - "net/http" "testing" - "github.com/stretchr/testify/require" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_set_override" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestUnauthorizedAccess(t *testing.T) { - h := testutil.NewHarness(t) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - } - - h.Register(route) - - t.Run("invalid authorization token", func(t *testing.T) { - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {"Bearer invalid_token"}, - } - - req := handler.Request{ - Namespace: uid.New("test"), - Identifier: "test_identifier", - Limit: 10, - Duration: 1000, - } - - res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, http.StatusUnauthorized, res.Status, "expected status code to be 401, got: %s", res.RawBody) - require.NotNil(t, res.Body) - }) - + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + } + }, + func() handler.Request { + return handler.Request{ + Namespace: uid.New("test"), + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, + } + }, + ) } diff --git a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go index fc49558b45..6a0e62e3be 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go @@ -2,65 +2,60 @@ package handler_test import ( "context" - "fmt" - "net/http" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_set_override" "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/testutil/authz" "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" ) func TestWorkspacePermissions(t *testing.T) { - ctx := context.Background() - h := testutil.NewHarness(t) - - // Create a namespace in the default workspace - namespaceID := uid.New(uid.RatelimitNamespacePrefix) - namespaceName := uid.New("name") - err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ - ID: namespaceID, - WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: namespaceName, - CreatedAt: time.Now().UnixMilli(), - }) - require.NoError(t, err) - - route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceCache: h.Caches.RatelimitNamespace, - } - - h.Register(route) - - // Create a key for a different workspace - differentWorkspace := h.CreateWorkspace() - differentWorkspaceKey := h.CreateRootKey(differentWorkspace.ID) - - headers := http.Header{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, - } - - // Try to create an override using a namespace from the default workspace - // but with a key from a different workspace - req := handler.Request{ - Namespace: namespaceID, - Identifier: "test_identifier", - Limit: 10, - Duration: 1000, - } - - res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) - - // This should return a 404 Not Found (for security reasons we don't reveal if the namespace exists) - require.Equal(t, http.StatusNotFound, res.Status, "got: %s", res.RawBody) - require.NotNil(t, res.Body) + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, + } + }, + RequiredPermissions: []string{"ratelimit.*.set_override"}, + SetupResources: func(h *testutil.Harness) authz.TestResources { + ctx := context.Background() + + // Create a namespace + namespaceID := uid.New(uid.RatelimitNamespacePrefix) + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: uid.New("name"), + CreatedAt: time.Now().UnixMilli(), + }) + if err != nil { + t.Fatalf("failed to create namespace: %v", err) + } + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + Custom: map[string]string{ + "namespace_id": namespaceID, + }, + } + }, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Namespace: res.Custom["namespace_id"], + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, + } + }, + }, + ) } diff --git a/go/pkg/testutil/authz/README.md b/go/pkg/testutil/authz/README.md new file mode 100644 index 0000000000..6374b2a4a5 --- /dev/null +++ b/go/pkg/testutil/authz/README.md @@ -0,0 +1,248 @@ +# Authorization Test Helpers + +This package provides generic, reusable test helpers for authentication (401) and authorization (403) testing across all API endpoints. + +## Overview + +Instead of writing repetitive test code for each endpoint, these helpers automatically test all common authentication and authorization scenarios with minimal configuration. + +## Benefits + +- **Consistent test coverage**: All endpoints test the same scenarios +- **Single source of truth**: Update test logic in one place +- **Type-safe with generics**: Compile-time type checking for requests and responses +- **Extensible**: Easy to add custom test cases + +## Usage + +### Basic 401 Test (Authentication Failures) + +For endpoints that don't need existing resources: + +```go +func TestCreateApiUnauthorized(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + func() handler.Request { + return handler.Request{ + Name: "test-api", + } + }, + ) +} +``` + +This automatically tests: + +- Invalid bearer token +- Nonexistent key +- Bearer with extra spaces +- Missing authorization header +- Empty authorization header +- Malformed header - no Bearer prefix +- Malformed header - Bearer only + +### Basic 403 Test (Authorization Failures) + +For simple endpoints without resource dependencies: + +```go +func TestCreateApiForbidden(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + } + }, + RequiredPermissions: []string{"api.*.create_api"}, + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + Name: "test-api", + } + }, + }, + ) +} +``` + +This automatically tests: + +- No permissions +- Unrelated permissions +- Wrong action (e.g., `read` instead of `create`) +- Permission for different resource +- Permission combinations (exact match, with extras, insufficient) + +### Advanced 403 Test (With Resource Setup) + +For endpoints that need existing resources: + +```go +func TestUpdateKeyForbidden(t *testing.T) { + authz.Test403(t, + authz.PermissionTestConfig[handler.Request, handler.Response]{ + SetupHandler: func(h *testutil.Harness) zen.Route { + return &handler.Handler{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + Vault: h.Vault, + } + }, + RequiredPermissions: []string{"api.*.update_key"}, + + // Setup creates resources needed for the test + SetupResources: func(h *testutil.Harness) authz.TestResources { + api := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + key := h.CreateKey(seed.CreateKeyRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + KeyAuthID: api.KeyAuthID.String, + }) + otherApi := h.CreateApi(seed.CreateApiRequest{ + WorkspaceID: h.Resources().UserWorkspace.ID, + }) + + return authz.TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + ApiID: api.ID, + KeyID: key.KeyID, + OtherApiID: otherApi.ID, + } + }, + + // Request uses the created resources + CreateRequest: func(res authz.TestResources) handler.Request { + return handler.Request{ + KeyId: res.KeyID, + Name: ptr.String("updated-name"), + } + }, + + // Optional: Add custom test cases + AdditionalPermissionTests: []authz.PermissionTestCase[handler.Request]{ + { + Name: "special scenario", + Permissions: []string{"some.permission"}, + ModifyRequest: func(req handler.Request) handler.Request { + req.SomeField = "modified" + return req + }, + ExpectedStatus: 403, + }, + }, + }, + ) +} +``` + +## TestResources Structure + +The `TestResources` struct provides commonly used resource IDs: + +```go +type TestResources struct { + WorkspaceID string // Primary workspace ID + OtherWorkspaceID string // For cross-workspace tests + ApiID string // Primary API ID + OtherApiID string // For cross-resource tests + KeyAuthID string // Primary key auth ID + KeyID string // Primary key ID + IdentityID string // Primary identity ID + Custom map[string]string // For endpoint-specific IDs +} +``` + +## Configuration Options + +### PermissionTestConfig + +- **SetupHandler**: Creates the handler with dependencies (required) +- **RequiredPermissions**: List of permissions needed (required) +- **CreateRequest**: Creates a valid request body (required) +- **SetupResources**: Optional, creates test data before running tests +- **AdditionalPermissionTests**: Optional, custom test cases beyond standard ones + +### PermissionTestCase + +- **Name**: Test case name +- **Permissions**: Permissions to grant +- **ModifyRequest**: Optional, modify the request for this test +- **ExpectedStatus**: Expected HTTP status code +- **ValidateError**: Optional, custom error validation + +## Migration Guide + +### Before (Old Pattern) + +```go +func TestCreateApiUnauthorized(t *testing.T) { + h := testutil.NewHarness(t) + route := &handler.Handler{...} + h.Register(route) + + t.Run("invalid bearer token", func(t *testing.T) { + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {"Bearer invalid_token"}, + } + req := handler.Request{Name: "test-api"} + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusUnauthorized, res.Status) + }) + + // ... 6 more similar test cases (50+ lines) +} +``` + +### After (New Pattern) + +```go +func TestCreateApiUnauthorized(t *testing.T) { + authz.Test401[handler.Request, handler.Response](t, + func(h *testutil.Harness) zen.Route { + return &handler.Handler{DB: h.DB, Keys: h.Keys, Logger: h.Logger} + }, + func() handler.Request { + return handler.Request{Name: "test-api"} + }, + ) +} +``` + +## Examples + +See these endpoints for reference implementations: + +- **Simple**: `apps/api/routes/v2_apis_create_api/*_test.go` +- **Medium**: `apps/api/routes/v2_keys_create_key/*_test.go` +- **Complex**: `apps/api/routes/v2_keys_update_key/*_test.go` (when implemented) + +## Remaining Work + +To complete the migration: + +1. Refactor remaining ~65 test files following the patterns above +2. Each endpoint should take ~5-10 minutes to refactor +3. Run tests after each refactor to ensure no regressions + +Endpoints to refactor: + +- `v2_keys_*` (30 endpoints) +- `v2_apis_*` (3 remaining) +- `v2_identities_*` (6 endpoints) +- `v2_permissions_*` (8 endpoints) +- `v2_ratelimit_*` (5 endpoints) diff --git a/go/pkg/testutil/authz/authz.go b/go/pkg/testutil/authz/authz.go new file mode 100644 index 0000000000..2486895fb8 --- /dev/null +++ b/go/pkg/testutil/authz/authz.go @@ -0,0 +1,315 @@ +package authz + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/apps/api/openapi" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +// Test401 runs a comprehensive suite of authentication failure tests. +// It tests all common scenarios where requests should return 401 Unauthorized. +// +// Type parameters: +// - TReq: The request type for the endpoint +// - TRes: The response type for the endpoint +// +// Parameters: +// - t: The testing context +// - setupHandler: Function to create the handler with test dependencies +// - createRequest: Function to create a valid request body +// +// Example usage: +// +// func TestCreateApiUnauthorized(t *testing.T) { +// authz.Test401[handler.Request, handler.Response](t, +// func(h *testutil.Harness) zen.Route { +// return &handler.Handler{DB: h.DB, Keys: h.Keys, Logger: h.Logger} +// }, +// func() handler.Request { +// return handler.Request{Name: "test-api"} +// }, +// ) +// } +func Test401[TReq any, TRes any]( + t *testing.T, + setupHandler func(*testutil.Harness) zen.Route, + createRequest func() TReq, +) { + t.Helper() + + testCases := []struct { + name string + authorization string + expectedStatus int + }{ + { + name: "invalid bearer token", + authorization: "Bearer invalid_token_12345", + expectedStatus: http.StatusUnauthorized, + }, + { + name: "nonexistent key", + authorization: fmt.Sprintf("Bearer %s", uid.New(uid.KeyPrefix)), + expectedStatus: http.StatusUnauthorized, + }, + { + name: "bearer with extra spaces", + authorization: "Bearer invalid_key_with_spaces ", + expectedStatus: http.StatusUnauthorized, + }, + { + name: "missing authorization header", + authorization: "", + expectedStatus: http.StatusBadRequest, // Missing auth typically returns 400 + }, + { + name: "empty authorization header", + authorization: " ", + expectedStatus: http.StatusBadRequest, + }, + { + name: "malformed authorization header - no Bearer prefix", + authorization: "invalid_token_without_bearer", + expectedStatus: http.StatusBadRequest, + }, + { + name: "malformed authorization header - Bearer only", + authorization: "Bearer", + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + h := testutil.NewHarness(t) + route := setupHandler(h) + h.Register(route) + + headers := http.Header{ + "Content-Type": {"application/json"}, + } + if tc.authorization != "" { + headers.Set("Authorization", tc.authorization) + } + + req := createRequest() + res := testutil.CallRoute[TReq, openapi.UnauthorizedErrorResponse](h, route, headers, req) + + require.Equal(t, tc.expectedStatus, res.Status, + "expected %d, got %d, body: %s", tc.expectedStatus, res.Status, res.RawBody) + require.NotNil(t, res.Body) + }) + } +} + +// Test403 runs a comprehensive suite of authorization failure tests. +// It tests all common scenarios where requests should return 403 Forbidden. +// +// Type parameters: +// - TReq: The request type for the endpoint +// - TRes: The response type for the endpoint +// +// Parameters: +// - t: The testing context +// - config: Configuration for the permission tests +// +// The test suite automatically generates test cases for: +// - No permissions +// - Wrong permissions (different action) +// - Unrelated permissions +// - Resource-specific permissions (if applicable) +// - Cross-workspace scenarios +// - Permission combinations +// +// Example usage: +// +// func TestCreateApiForbidden(t *testing.T) { +// authz.Test403(t, +// authz.PermissionTestConfig[handler.Request, handler.Response]{ +// SetupHandler: func(h *testutil.Harness) zen.Route { +// return &handler.Handler{DB: h.DB, Keys: h.Keys, Logger: h.Logger} +// }, +// RequiredPermissions: []string{"api.*.create_api"}, +// CreateRequest: func(res authz.TestResources) handler.Request { +// return handler.Request{Name: "test-api"} +// }, +// }, +// ) +// } +func Test403[TReq any, TRes any]( + t *testing.T, + config PermissionTestConfig[TReq, TRes], +) { + t.Helper() + + h := testutil.NewHarness(t) + route := config.SetupHandler(h) + h.Register(route) + + // Setup resources + var resources TestResources + if config.SetupResources != nil { + resources = config.SetupResources(h) + } else { + resources = TestResources{ + WorkspaceID: h.Resources().UserWorkspace.ID, + } + } + + // Generate standard test cases + testCases := generateStandardPermissionTests[TReq](config.RequiredPermissions, resources) + + // Add any additional custom test cases + for _, customTest := range config.AdditionalPermissionTests { + testCases = append(testCases, customTest) + } + + // Run all test cases + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + // Create root key with specified permissions + rootKey := h.CreateRootKey(resources.WorkspaceID, tc.Permissions...) + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + req := config.CreateRequest(resources) + if tc.ModifyRequest != nil { + req = tc.ModifyRequest(req, resources) + } + + res := testutil.CallRoute[TReq, openapi.ForbiddenErrorResponse](h, route, headers, req) + + require.Equal(t, tc.ExpectedStatus, res.Status, + "expected %d, got %d, body: %s", tc.ExpectedStatus, res.Status, res.RawBody) + + if tc.ExpectedStatus == http.StatusForbidden { + require.NotNil(t, res.Body) + require.NotNil(t, res.Body.Error) + if tc.ValidateError != nil { + tc.ValidateError(t, res.Body.Error.Detail) + } + } + }) + } + + // Test permission combinations (some should pass, some should fail) + t.Run("permission combinations", func(t *testing.T) { + testPermissionCombinations(t, h, route, config, resources) + }) +} + +// generateStandardPermissionTests creates standard test cases based on required permissions +func generateStandardPermissionTests[TReq any](requiredPermissions []string, resources TestResources) []PermissionTestCase[TReq] { + testCases := []PermissionTestCase[TReq]{ + { + Name: "no permissions", + Permissions: []string{}, + ExpectedStatus: http.StatusForbidden, + }, + { + Name: "unrelated permission", + Permissions: []string{"completely.unrelated.permission"}, + ExpectedStatus: http.StatusForbidden, + }, + } + + // For each required permission, generate related test cases + for _, perm := range requiredPermissions { + parts := strings.Split(perm, ".") + if len(parts) >= 3 { + // Generate wrong action test (e.g., api.*.read instead of api.*.create) + wrongAction := parts[0] + "." + parts[1] + ".wrong_action" + testCases = append(testCases, PermissionTestCase[TReq]{ + Name: fmt.Sprintf("wrong action for %s", parts[0]), + Permissions: []string{wrongAction}, + ExpectedStatus: http.StatusForbidden, + }) + + // If permission uses wildcard and we have a specific resource ID, test mismatch + if parts[1] == "*" && resources.ApiID != "" { + otherID := uid.New(uid.APIPrefix) + specificWrong := parts[0] + "." + otherID + "." + parts[2] + testCases = append(testCases, PermissionTestCase[TReq]{ + Name: "permission for different resource", + Permissions: []string{specificWrong}, + ExpectedStatus: http.StatusForbidden, + }) + } + } + } + + return testCases +} + +// testPermissionCombinations tests various combinations of permissions +func testPermissionCombinations[TReq any, TRes any]( + t *testing.T, + h *testutil.Harness, + route zen.Route, + config PermissionTestConfig[TReq, TRes], + resources TestResources, +) { + t.Helper() + + if len(config.RequiredPermissions) == 0 { + return + } + + testCases := []struct { + name string + permissions []string + shouldPass bool + }{ + { + name: "exact required permission", + permissions: config.RequiredPermissions, + shouldPass: true, + }, + { + name: "required permission plus additional", + permissions: append([]string{"some.other.permission"}, config.RequiredPermissions...), + shouldPass: true, + }, + { + name: "only additional permissions", + permissions: []string{ + "some.other.permission", + "another.permission", + }, + shouldPass: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rootKey := h.CreateRootKey(resources.WorkspaceID, tc.permissions...) + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + req := config.CreateRequest(resources) + res := testutil.CallRoute[TReq, TRes](h, route, headers, req) + + if tc.shouldPass { + require.NotEqual(t, http.StatusForbidden, res.Status, + "expected success, got %d, body: %s", res.Status, res.RawBody) + require.NotEqual(t, http.StatusUnauthorized, res.Status, + "expected success, got %d, body: %s", res.Status, res.RawBody) + } else { + require.Equal(t, http.StatusForbidden, res.Status, + "expected 403, got %d, body: %s", res.Status, res.RawBody) + } + }) + } +} diff --git a/go/pkg/testutil/authz/types.go b/go/pkg/testutil/authz/types.go new file mode 100644 index 0000000000..32b870ee4e --- /dev/null +++ b/go/pkg/testutil/authz/types.go @@ -0,0 +1,86 @@ +package authz + +import ( + "testing" + + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +// TestResources contains common resource IDs that tests might need. +// Tests can use these IDs in their requests and permission checks. +type TestResources struct { + // WorkspaceID is the primary workspace ID for testing + WorkspaceID string + + // OtherWorkspaceID is a secondary workspace for cross-workspace testing + OtherWorkspaceID string + + // ApiID is the primary API ID for testing + ApiID string + + // OtherApiID is a secondary API for cross-resource testing + OtherApiID string + + // KeyAuthID is the primary key auth ID + KeyAuthID string + + // KeyID is the primary key ID for testing + KeyID string + + // IdentityID is the primary identity ID for testing + IdentityID string + + // Custom can store any additional resource IDs specific to a test + Custom map[string]string +} + +// PermissionTestConfig configures a 403 authorization test suite. +// TReq is the request type, TRes is the response type. +type PermissionTestConfig[TReq any, TRes any] struct { + // SetupHandler creates the handler with injected dependencies + SetupHandler func(*testutil.Harness) zen.Route + + // RequiredPermissions is the list of permissions required to access this endpoint. + // The test will automatically generate scenarios to test these permissions. + RequiredPermissions []string + + // CreateRequest creates a valid request using the provided test resources. + // The resources parameter contains IDs of created test data. + CreateRequest func(resources TestResources) TReq + + // SetupResources optionally sets up test data before running tests. + // Returns resource IDs that can be used in CreateRequest and permission checks. + // If nil, uses default resources from harness.Resources(). + SetupResources func(*testutil.Harness) TestResources + + // AdditionalPermissionTests adds custom permission test cases beyond the standard ones. + AdditionalPermissionTests []PermissionTestCase[TReq] +} + +// PermissionTestCase represents a custom permission test scenario. +type PermissionTestCase[TReq any] struct { + // Name of the test case + Name string + + // Permissions to grant to the root key for this test + Permissions []string + + // ModifyRequest optionally modifies the request for this specific test + ModifyRequest func(TReq, TestResources) TReq + + // ExpectedStatus is the expected HTTP status code + ExpectedStatus int + + // ValidateError optionally validates the error response + ValidateError func(t *testing.T, errorDetail string) +} + +// Auth401TestConfig configures a 401 authentication test suite. +type Auth401TestConfig[TReq any, TRes any] struct { + // SetupHandler creates the handler with injected dependencies + SetupHandler func(*testutil.Harness) zen.Route + + // CreateRequest creates a valid request for testing + CreateRequest func() TReq +}