diff --git a/apps/docs/errors/unkey/data/permission_already_exists.mdx b/apps/docs/errors/unkey/data/permission_already_exists.mdx index 51b44047b9..da47abe0c9 100644 --- a/apps/docs/errors/unkey/data/permission_already_exists.mdx +++ b/apps/docs/errors/unkey/data/permission_already_exists.mdx @@ -9,7 +9,7 @@ description: "A permission with this slug already exists" "requestId": "req_2c9a0jf23l4k567" }, "error": { - "detail": "A permission with name \"admin\" already exists in this workspace", + "detail": "A permission with slug \"admin\" already exists in this workspace", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/permission/duplicate" diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go index f29414e91f..d3a8fe187d 100644 --- a/go/apps/api/openapi/gen.go +++ b/go/apps/api/openapi/gen.go @@ -353,9 +353,6 @@ type RatelimitOverride struct { // - Custom tier-based limits for different customer segments Limit int64 `json:"limit"` - // NamespaceId The unique identifier of the rate limit namespace this override belongs to. This links the override to a specific namespace context, ensuring the override only applies within that namespace. - NamespaceId string `json:"namespaceId"` - // OverrideId The unique identifier of this specific rate limit override. This ID is generated when the override is created and can be used for management operations like updating or deleting the override. OverrideId string `json:"overrideId"` } @@ -630,7 +627,10 @@ type V2IdentitiesCreateIdentityResponseBody struct { } // V2IdentitiesCreateIdentityResponseData defines model for V2IdentitiesCreateIdentityResponseData. -type V2IdentitiesCreateIdentityResponseData = map[string]interface{} +type V2IdentitiesCreateIdentityResponseData struct { + // IdentityId The unique identifier of the created identity. + IdentityId string `json:"identityId"` +} // V2IdentitiesDeleteIdentityRequestBody defines model for V2IdentitiesDeleteIdentityRequestBody. type V2IdentitiesDeleteIdentityRequestBody struct { diff --git a/go/apps/api/openapi/openapi-generated.yaml b/go/apps/api/openapi/openapi-generated.yaml index a9ca263690..6c110fa2b5 100644 --- a/go/apps/api/openapi/openapi-generated.yaml +++ b/go/apps/api/openapi/openapi-generated.yaml @@ -674,7 +674,7 @@ components: type: string minLength: 1 maxLength: 100 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ description: | Assigns existing roles to this key for permission management through role-based access control. Roles must already exist in your workspace before assignment. @@ -916,7 +916,7 @@ components: Invalid role references cause the entire operation to fail atomically, ensuring consistent state. items: type: string - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ minLength: 3 maxLength: 255 description: Specify the role by name. @@ -1004,7 +1004,7 @@ components: Invalid role references cause the entire operation to fail atomically, ensuring consistent state. items: type: string - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ minLength: 3 maxLength: 255 description: Specify the role by name. @@ -1171,7 +1171,7 @@ components: type: string minLength: 1 maxLength: 100 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ description: | Assigns existing roles to this key for permission management through role-based access control. Roles must already exist in your workspace before assignment. @@ -1459,7 +1459,7 @@ components: properties: role: type: string - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ minLength: 3 maxLength: 255 description: | @@ -1523,7 +1523,7 @@ components: type: string minLength: 3 maxLength: 255 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ description: | Unique identifier of the role to permanently delete from your workspace. Must either be a valid role ID that begins with 'role_' or the given role name and exists within your workspace. @@ -2195,7 +2195,6 @@ components: type: string minLength: 1 maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" description: Human-readable name for this rate limit. example: api_requests limit: @@ -2312,6 +2311,12 @@ components: default: false V2IdentitiesCreateIdentityResponseData: type: object + properties: + identityId: + type: string + description: The unique identifier of the created identity. + required: + - identityId V2IdentitiesGetIdentityResponseData: type: object required: @@ -2703,7 +2708,6 @@ components: type: string minLength: 1 maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" description: Human-readable name for this rate limit. example: api_requests limit: @@ -2874,11 +2878,6 @@ components: type: object additionalProperties: false properties: - namespaceId: - description: The unique identifier of the rate limit namespace this override belongs to. This links the override to a specific namespace context, ensuring the override only applies within that namespace. - type: string - minLength: 1 - maxLength: 255 overrideId: description: The unique identifier of this specific rate limit override. This ID is generated when the override is created and can be used for management operations like updating or deleting the override. type: string @@ -2919,7 +2918,6 @@ components: type: integer minimum: 0 required: - - namespaceId - overrideId - duration - identifier diff --git a/go/apps/api/openapi/spec/common/ratelimitOverride.yaml b/go/apps/api/openapi/spec/common/ratelimitOverride.yaml index 369ad8fd81..c51ad1e63e 100644 --- a/go/apps/api/openapi/spec/common/ratelimitOverride.yaml +++ b/go/apps/api/openapi/spec/common/ratelimitOverride.yaml @@ -1,13 +1,6 @@ type: object additionalProperties: false properties: - namespaceId: - description: The unique identifier of the rate limit namespace this override - belongs to. This links the override to a specific namespace context, ensuring - the override only applies within that namespace. - type: string - minLength: 1 - maxLength: 255 overrideId: description: The unique identifier of this specific rate limit override. This ID is generated when the override is created and can be used for @@ -53,7 +46,6 @@ properties: type: integer minimum: 0 required: - - namespaceId - overrideId - duration - identifier diff --git a/go/apps/api/openapi/spec/common/ratelimitResponse.yaml b/go/apps/api/openapi/spec/common/ratelimitResponse.yaml index bbf2e5ac87..7bcdd37653 100644 --- a/go/apps/api/openapi/spec/common/ratelimitResponse.yaml +++ b/go/apps/api/openapi/spec/common/ratelimitResponse.yaml @@ -11,7 +11,6 @@ properties: type: string minLength: 1 maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" description: Human-readable name for this rate limit. example: api_requests limit: diff --git a/go/apps/api/openapi/spec/paths/v2/identities/createIdentity/V2IdentitiesCreateIdentityResponseData.yaml b/go/apps/api/openapi/spec/paths/v2/identities/createIdentity/V2IdentitiesCreateIdentityResponseData.yaml index 91bf3091f7..5ebaf15f57 100644 --- a/go/apps/api/openapi/spec/paths/v2/identities/createIdentity/V2IdentitiesCreateIdentityResponseData.yaml +++ b/go/apps/api/openapi/spec/paths/v2/identities/createIdentity/V2IdentitiesCreateIdentityResponseData.yaml @@ -1 +1,7 @@ type: object +properties: + identityId: + type: string + description: The unique identifier of the created identity. +required: + - identityId diff --git a/go/apps/api/openapi/spec/paths/v2/keys/createKey/V2KeysCreateKeyRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/keys/createKey/V2KeysCreateKeyRequestBody.yaml index 7991e7b30b..09d89085a4 100644 --- a/go/apps/api/openapi/spec/paths/v2/keys/createKey/V2KeysCreateKeyRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/keys/createKey/V2KeysCreateKeyRequestBody.yaml @@ -78,7 +78,7 @@ properties: type: string minLength: 1 maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ description: | Assigns existing roles to this key for permission management through role-based access control. Roles must already exist in your workspace before assignment. diff --git a/go/apps/api/openapi/spec/paths/v2/keys/removeRoles/V2KeysRemoveRolesRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/keys/removeRoles/V2KeysRemoveRolesRequestBody.yaml index ab1df769a7..043346e57a 100644 --- a/go/apps/api/openapi/spec/paths/v2/keys/removeRoles/V2KeysRemoveRolesRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/keys/removeRoles/V2KeysRemoveRolesRequestBody.yaml @@ -26,7 +26,7 @@ properties: Invalid role references cause the entire operation to fail atomically, ensuring consistent state. items: type: string - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ minLength: 3 maxLength: 255 description: Specify the role by name. diff --git a/go/apps/api/openapi/spec/paths/v2/keys/setRoles/V2KeysSetRolesRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/keys/setRoles/V2KeysSetRolesRequestBody.yaml index 82cd248731..e56e062c2b 100644 --- a/go/apps/api/openapi/spec/paths/v2/keys/setRoles/V2KeysSetRolesRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/keys/setRoles/V2KeysSetRolesRequestBody.yaml @@ -26,7 +26,7 @@ properties: Invalid role references cause the entire operation to fail atomically, ensuring consistent state. items: type: string - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ minLength: 3 maxLength: 255 description: Specify the role by name. diff --git a/go/apps/api/openapi/spec/paths/v2/keys/updateKey/V2KeysUpdateKeyRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/keys/updateKey/V2KeysUpdateKeyRequestBody.yaml index a6c5063ae0..17992ace9e 100644 --- a/go/apps/api/openapi/spec/paths/v2/keys/updateKey/V2KeysUpdateKeyRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/keys/updateKey/V2KeysUpdateKeyRequestBody.yaml @@ -104,7 +104,7 @@ properties: type: string minLength: 1 maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ description: | Assigns existing roles to this key for permission management through role-based access control. Roles must already exist in your workspace before assignment. diff --git a/go/apps/api/openapi/spec/paths/v2/keys/verifyKey/VerifyKeyRatelimitData.yaml b/go/apps/api/openapi/spec/paths/v2/keys/verifyKey/VerifyKeyRatelimitData.yaml index 741961952d..29ae136919 100644 --- a/go/apps/api/openapi/spec/paths/v2/keys/verifyKey/VerifyKeyRatelimitData.yaml +++ b/go/apps/api/openapi/spec/paths/v2/keys/verifyKey/VerifyKeyRatelimitData.yaml @@ -14,7 +14,6 @@ properties: type: string minLength: 1 maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" description: Human-readable name for this rate limit. example: api_requests limit: diff --git a/go/apps/api/openapi/spec/paths/v2/permissions/deleteRole/V2PermissionsDeleteRoleRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/permissions/deleteRole/V2PermissionsDeleteRoleRequestBody.yaml index 265fa4498b..df154db9d8 100644 --- a/go/apps/api/openapi/spec/paths/v2/permissions/deleteRole/V2PermissionsDeleteRoleRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/permissions/deleteRole/V2PermissionsDeleteRoleRequestBody.yaml @@ -4,7 +4,7 @@ required: properties: role: type: string - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ minLength: 3 maxLength: 255 description: | diff --git a/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleRequestBody.yaml index 3e51da844a..256c931192 100644 --- a/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleRequestBody.yaml @@ -6,7 +6,7 @@ properties: type: string minLength: 3 maxLength: 255 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" + pattern: ^[a-zA-Z0-9_:\-\.\*]+$ description: | Unique identifier of the role to permanently delete from your workspace. Must either be a valid role ID that begins with 'role_' or the given role name and exists within your workspace. diff --git a/go/apps/api/routes/v2_apis_delete_api/handler.go b/go/apps/api/routes/v2_apis_delete_api/handler.go index 6c16c82027..9c047757a8 100644 --- a/go/apps/api/routes/v2_apis_delete_api/handler.go +++ b/go/apps/api/routes/v2_apis_delete_api/handler.go @@ -161,5 +161,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { Meta: openapi.Meta{ RequestId: s.RequestID(), }, + Data: openapi.EmptyResponse{}, }) } diff --git a/go/apps/api/routes/v2_apis_list_keys/handler.go b/go/apps/api/routes/v2_apis_list_keys/handler.go index 3f491e521b..06ea93fc68 100644 --- a/go/apps/api/routes/v2_apis_list_keys/handler.go +++ b/go/apps/api/routes/v2_apis_list_keys/handler.go @@ -305,30 +305,18 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } } - // Filter out the cursor key if cursor was provided (to avoid duplicates) - filteredKeys := keys - if cursor != "" { - var filtered []db.ListKeysByKeyAuthIDRow - for _, key := range keys { - if key.Key.ID != cursor { - filtered = append(filtered, key) - } - } - filteredKeys = filtered - } - - // Determine the actual number of keys to return (respect the limit) - numKeysToReturn := len(filteredKeys) - hasMore := false - if len(filteredKeys) > limit { - numKeysToReturn = limit - hasMore = true + // Determine the cursor for the next page + hasMore := len(keys) > limit + var nextCursor *string + if hasMore { + nextCursor = ptr.P(keys[len(keys)-1].Key.ID) + // Trim the results to the requested limit + keys = keys[:limit] } // Transform keys into the response format - responseData := make([]openapi.KeyResponseData, numKeysToReturn) - for i := 0; i < numKeysToReturn; i++ { - key := filteredKeys[i] + responseData := make([]openapi.KeyResponseData, len(keys)) + for i, key := range keys { k := openapi.KeyResponseData{ KeyId: key.Key.ID, Start: key.Key.Start, @@ -469,13 +457,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { responseData[i] = k } - // Determine the cursor for the next page - var nextCursor *string - if hasMore && numKeysToReturn > 0 { - cursor := responseData[numKeysToReturn-1].KeyId - nextCursor = &cursor - } - return s.JSON(http.StatusOK, Response{ Meta: openapi.Meta{ RequestId: s.RequestID(), diff --git a/go/apps/api/routes/v2_identities_create_identity/handler.go b/go/apps/api/routes/v2_identities_create_identity/handler.go index 3c042cb133..b1255ad797 100644 --- a/go/apps/api/routes/v2_identities_create_identity/handler.go +++ b/go/apps/api/routes/v2_identities_create_identity/handler.go @@ -94,8 +94,9 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { meta = rawMeta } + identityID := uid.New(uid.IdentityPrefix) + err = db.Tx(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) error { - identityID := uid.New(uid.IdentityPrefix) args := db.InsertIdentityParams{ ID: identityID, ExternalID: req.ExternalId, @@ -110,7 +111,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if db.IsDuplicateKeyError(err) { return fault.Wrap(err, fault.Code(codes.Data.Identity.Duplicate.URN()), - fault.Internal("identity already exists"), fault.Public(fmt.Sprintf("Identity with externalId \"%s\" already exists in this workspace.", req.ExternalId)), + fault.Internal("identity already exists"), fault.Public(fmt.Sprintf("Identity with externalId '%s' already exists in this workspace.", req.ExternalId)), ) } @@ -213,6 +214,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { Meta: openapi.Meta{ RequestId: s.RequestID(), }, - Data: openapi.V2IdentitiesCreateIdentityResponseData{}, + Data: openapi.V2IdentitiesCreateIdentityResponseData{IdentityId: identityID}, }) } diff --git a/go/apps/api/routes/v2_identities_update_identity/handler.go b/go/apps/api/routes/v2_identities_update_identity/handler.go index 8ffe3fa2f8..21b855dc26 100644 --- a/go/apps/api/routes/v2_identities_update_identity/handler.go +++ b/go/apps/api/routes/v2_identities_update_identity/handler.go @@ -83,7 +83,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return fault.New("duplicate ratelimit name", fault.Code(codes.App.Validation.InvalidInput.URN()), fault.Internal("duplicate ratelimit name"), - fault.Public(fmt.Sprintf("Ratelimit with name %q is already defined in the request", ratelimit.Name)), + fault.Public(fmt.Sprintf("Ratelimit with name '%s' is already defined in the request", ratelimit.Name)), ) } nameSet[ratelimit.Name] = true diff --git a/go/apps/api/routes/v2_keys_add_roles/handler.go b/go/apps/api/routes/v2_keys_add_roles/handler.go index d6b0654b5d..3a597cd8bb 100644 --- a/go/apps/api/routes/v2_keys_add_roles/handler.go +++ b/go/apps/api/routes/v2_keys_add_roles/handler.go @@ -143,7 +143,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return fault.New("role not found", fault.Code(codes.Data.Role.NotFound.URN()), - fault.Internal("role not found"), fault.Public(fmt.Sprintf("Role %q was not found.", role)), + fault.Internal("role not found"), fault.Public(fmt.Sprintf("Role '%s' was not found.", role)), ) } diff --git a/go/apps/api/routes/v2_keys_create_key/handler.go b/go/apps/api/routes/v2_keys_create_key/handler.go index 2aa2889525..a841fc41cf 100644 --- a/go/apps/api/routes/v2_keys_create_key/handler.go +++ b/go/apps/api/routes/v2_keys_create_key/handler.go @@ -492,7 +492,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return fault.New("role not found", fault.Code(codes.Data.Role.NotFound.URN()), - fault.Internal("role not found"), fault.Public(fmt.Sprintf("Role %q was not found.", requestedName)), + fault.Internal("role not found"), fault.Public(fmt.Sprintf("Role '%s' was not found.", requestedName)), ) } diff --git a/go/apps/api/routes/v2_keys_remove_permissions/handler.go b/go/apps/api/routes/v2_keys_remove_permissions/handler.go index 3ea23294cc..5117f1146d 100644 --- a/go/apps/api/routes/v2_keys_remove_permissions/handler.go +++ b/go/apps/api/routes/v2_keys_remove_permissions/handler.go @@ -152,7 +152,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if !exists { return fault.New("permission not found", fault.Code(codes.Data.Permission.NotFound.URN()), - fault.Public(fmt.Sprintf("Permission %q was not found.", toRemove)), + fault.Public(fmt.Sprintf("Permission '%s' was not found.", toRemove)), ) } } diff --git a/go/apps/api/routes/v2_keys_remove_roles/handler.go b/go/apps/api/routes/v2_keys_remove_roles/handler.go index 2f1b610dbc..4efe4ca56d 100644 --- a/go/apps/api/routes/v2_keys_remove_roles/handler.go +++ b/go/apps/api/routes/v2_keys_remove_roles/handler.go @@ -136,7 +136,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if !exists { return fault.New("role not found", fault.Code(codes.Data.Role.NotFound.URN()), - fault.Public(fmt.Sprintf("Role %q was not found.", role)), + fault.Public(fmt.Sprintf("Role '%s' was not found.", role)), ) } } diff --git a/go/apps/api/routes/v2_keys_set_roles/404_test.go b/go/apps/api/routes/v2_keys_set_roles/404_test.go index 5a9ee9dd68..3a8d6536ba 100644 --- a/go/apps/api/routes/v2_keys_set_roles/404_test.go +++ b/go/apps/api/routes/v2_keys_set_roles/404_test.go @@ -116,7 +116,7 @@ func TestNotFoundErrors(t *testing.T) { require.NotNil(t, res.Body) require.NotNil(t, res.Body.Error) require.Equal(t, "Not Found", res.Body.Error.Title) - require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role %q was not found", nonExistentRoleID)) + require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role '%s' was not found", nonExistentRoleID)) require.Equal(t, 404, res.Body.Error.Status) }) @@ -140,7 +140,7 @@ func TestNotFoundErrors(t *testing.T) { require.NotNil(t, res.Body) require.NotNil(t, res.Body.Error) require.Equal(t, "Not Found", res.Body.Error.Title) - require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role %q was not found", nonExistentRoleName)) + require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role '%s' was not found", nonExistentRoleName)) require.Equal(t, 404, res.Body.Error.Status) }) @@ -248,7 +248,7 @@ func TestNotFoundErrors(t *testing.T) { require.NotNil(t, res.Body) require.NotNil(t, res.Body.Error) require.Equal(t, "Not Found", res.Body.Error.Title) - require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role %q was not found", otherRoleID)) + require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role '%s' was not found", otherRoleID)) require.Equal(t, 404, res.Body.Error.Status) }) @@ -270,7 +270,7 @@ func TestNotFoundErrors(t *testing.T) { require.NotNil(t, res.Body) require.NotNil(t, res.Body.Error) require.Equal(t, "Not Found", res.Body.Error.Title) - require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role %q was not found", otherRoleName)) + require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role '%s' was not found", otherRoleName)) require.Equal(t, 404, res.Body.Error.Status) }) }) @@ -295,7 +295,7 @@ func TestNotFoundErrors(t *testing.T) { require.NotNil(t, res.Body) require.NotNil(t, res.Body.Error) require.Equal(t, "Not Found", res.Body.Error.Title) - require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role %q was not found", nonExistentRoleID)) + require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role '%s' was not found", nonExistentRoleID)) require.Equal(t, 404, res.Body.Error.Status) }) @@ -345,7 +345,7 @@ func TestNotFoundErrors(t *testing.T) { require.NotNil(t, res.Body) require.NotNil(t, res.Body.Error) require.Equal(t, "Not Found", res.Body.Error.Title) - require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role %q was not found", validFormattedRoleID)) + require.Contains(t, res.Body.Error.Detail, fmt.Sprintf("Role '%s' was not found", validFormattedRoleID)) require.Equal(t, 404, res.Body.Error.Status) }) } diff --git a/go/apps/api/routes/v2_keys_set_roles/handler.go b/go/apps/api/routes/v2_keys_set_roles/handler.go index 7e110a4fe3..07267c7a62 100644 --- a/go/apps/api/routes/v2_keys_set_roles/handler.go +++ b/go/apps/api/routes/v2_keys_set_roles/handler.go @@ -131,7 +131,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if !exists { return fault.New("role not found", fault.Code(codes.Data.Role.NotFound.URN()), - fault.Public(fmt.Sprintf("Role %q was not found.", role)), + fault.Public(fmt.Sprintf("Role '%s' was not found.", role)), ) } } diff --git a/go/apps/api/routes/v2_keys_update_key/handler.go b/go/apps/api/routes/v2_keys_update_key/handler.go index a9a6e76fa4..855b3f2aa4 100644 --- a/go/apps/api/routes/v2_keys_update_key/handler.go +++ b/go/apps/api/routes/v2_keys_update_key/handler.go @@ -511,7 +511,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return fault.New("role not found", fault.Code(codes.Data.Role.NotFound.URN()), fault.Internal("role not found"), - fault.Public(fmt.Sprintf("Role %q was not found.", requestedName)), + fault.Public(fmt.Sprintf("Role '%s' was not found.", requestedName)), ) } diff --git a/go/apps/api/routes/v2_keys_verify_key/412_test.go b/go/apps/api/routes/v2_keys_verify_key/412_test.go index 1e2ca7a2dc..17dd241266 100644 --- a/go/apps/api/routes/v2_keys_verify_key/412_test.go +++ b/go/apps/api/routes/v2_keys_verify_key/412_test.go @@ -68,7 +68,7 @@ func TestPreconditionFailed(t *testing.T) { require.NotNil(t, res.Body.Error) // Should contain useful error message about missing ratelimit for key and identity - expectedMsg := fmt.Sprintf("ratelimit \"does-not-exist\" was requested but does not exist for key \"%s\" nor identity", key.KeyID) + expectedMsg := fmt.Sprintf("ratelimit 'does-not-exist' was requested but does not exist for key '%s' nor identity", key.KeyID) require.Contains(t, res.Body.Error.Detail, expectedMsg) require.Contains(t, res.Body.Error.Detail, identity) require.Contains(t, res.Body.Error.Detail, "test-missing-ratelimit") @@ -101,7 +101,7 @@ func TestPreconditionFailed(t *testing.T) { require.NotNil(t, res.Body.Error) // Should contain error message indicating no identity connected - expectedMsg := fmt.Sprintf("ratelimit \"does-not-exist\" was requested but does not exist for key \"%s\" and there is no identity connected", key.KeyID) + expectedMsg := fmt.Sprintf("ratelimit 'does-not-exist' was requested but does not exist for key '%s' and there is no identity connected", key.KeyID) require.Contains(t, res.Body.Error.Detail, expectedMsg) }) diff --git a/go/apps/api/routes/v2_permissions_create_permission/handler.go b/go/apps/api/routes/v2_permissions_create_permission/handler.go index 79796ac3c7..f7587bda80 100644 --- a/go/apps/api/routes/v2_permissions_create_permission/handler.go +++ b/go/apps/api/routes/v2_permissions_create_permission/handler.go @@ -2,6 +2,7 @@ package handler import ( "context" + "fmt" "net/http" "time" @@ -83,7 +84,8 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if db.IsDuplicateKeyError(err) { return fault.New("permission already exists", fault.Code(codes.Data.Permission.Duplicate.URN()), - fault.Internal("already exists"), fault.Public("A permission with name \""+req.Name+"\" already exists in this workspace"), + fault.Internal("already exists"), + fault.Public(fmt.Sprintf("A permission with slug '%s' already exists in this workspace", req.Slug)), ) } return fault.Wrap(err, diff --git a/go/apps/api/routes/v2_permissions_create_role/handler.go b/go/apps/api/routes/v2_permissions_create_role/handler.go index d91f1bf06a..aba867c379 100644 --- a/go/apps/api/routes/v2_permissions_create_role/handler.go +++ b/go/apps/api/routes/v2_permissions_create_role/handler.go @@ -90,7 +90,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if db.IsDuplicateKeyError(err) { return fault.New("role already exists", fault.Code(codes.Data.Role.Duplicate.URN()), - fault.Internal("role already exists"), fault.Public(fmt.Sprintf("A role with name %q already exists in this workspace", req.Name)), + fault.Internal("role already exists"), fault.Public(fmt.Sprintf("A role with name '%s' already exists in this workspace", req.Name)), ) } return fault.Wrap(err, diff --git a/go/apps/api/routes/v2_permissions_delete_permission/handler.go b/go/apps/api/routes/v2_permissions_delete_permission/handler.go index 676b1ad32b..f48feda0d2 100644 --- a/go/apps/api/routes/v2_permissions_delete_permission/handler.go +++ b/go/apps/api/routes/v2_permissions_delete_permission/handler.go @@ -147,5 +147,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { Meta: openapi.Meta{ RequestId: s.RequestID(), }, + Data: openapi.EmptyResponse{}, }) } diff --git a/go/apps/api/routes/v2_permissions_delete_role/handler.go b/go/apps/api/routes/v2_permissions_delete_role/handler.go index 955ae5735d..154491cbce 100644 --- a/go/apps/api/routes/v2_permissions_delete_role/handler.go +++ b/go/apps/api/routes/v2_permissions_delete_role/handler.go @@ -147,5 +147,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { Meta: openapi.Meta{ RequestId: s.RequestID(), }, + Data: openapi.EmptyResponse{}, }) } diff --git a/go/apps/api/routes/v2_permissions_list_permissions/handler.go b/go/apps/api/routes/v2_permissions_list_permissions/handler.go index 7148b78e54..1e02cf15b1 100644 --- a/go/apps/api/routes/v2_permissions_list_permissions/handler.go +++ b/go/apps/api/routes/v2_permissions_list_permissions/handler.go @@ -53,6 +53,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } cursor := ptr.SafeDeref(req.Cursor, "") + limit := ptr.SafeDeref(req.Limit, 100) err = auth.VerifyRootKey(ctx, keys.WithPermissions(rbac.Or( rbac.T(rbac.Tuple{ @@ -71,6 +72,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { db.ListPermissionsParams{ WorkspaceID: auth.AuthorizedWorkspaceID, IDCursor: cursor, + Limit: int32(limit) + 1, }, ) if err != nil { @@ -80,17 +82,14 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { ) } - // Check if we have more results by seeing if we got 101 permissions - hasMore := len(permissions) > 100 + hasMore := len(permissions) > limit var nextCursor *string - // If we have more than 100, truncate to 100 if hasMore { - nextCursor = ptr.P(permissions[100].ID) - permissions = permissions[:100] + nextCursor = ptr.P(permissions[limit].ID) + permissions = permissions[:limit] } - // 5. Transform permissions into response format responsePermissions := make([]openapi.Permission, 0, len(permissions)) for _, perm := range permissions { permission := openapi.Permission{ diff --git a/go/apps/api/routes/v2_permissions_list_roles/handler.go b/go/apps/api/routes/v2_permissions_list_roles/handler.go index 917debd1ac..9779151f2e 100644 --- a/go/apps/api/routes/v2_permissions_list_roles/handler.go +++ b/go/apps/api/routes/v2_permissions_list_roles/handler.go @@ -54,6 +54,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } cursor := ptr.SafeDeref(req.Cursor, "") + limit := ptr.SafeDeref(req.Limit, 100) err = auth.VerifyRootKey(ctx, keys.WithPermissions(rbac.Or( rbac.T(rbac.Tuple{ @@ -72,6 +73,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { db.ListRolesParams{ WorkspaceID: auth.AuthorizedWorkspaceID, IDCursor: cursor, + Limit: int32(limit) + 1, }, ) if err != nil { @@ -82,10 +84,10 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } var nextCursor *string - hasMore := len(roles) > 100 + hasMore := len(roles) > limit if hasMore { - nextCursor = ptr.P(roles[100].ID) - roles = roles[:100] + nextCursor = ptr.P(roles[limit].ID) + roles = roles[:limit] } roleResponses := make([]openapi.Role, 0, len(roles)) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go index 4e3e70757f..ebc2ff6dc8 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go @@ -73,7 +73,6 @@ func TestGetOverrideSuccessfully(t *testing.T) { require.Equal(t, 200, res.Status, "expected 200, received: %v", *res.Body) require.NotNil(t, res.Body) require.Equal(t, overrideID, res.Body.Data.OverrideId) - require.Equal(t, namespaceID, res.Body.Data.NamespaceId) require.Equal(t, identifier, res.Body.Data.Identifier) require.Equal(t, int64(limit), res.Body.Data.Limit) require.Equal(t, int64(duration), res.Body.Data.Duration) @@ -90,7 +89,6 @@ func TestGetOverrideSuccessfully(t *testing.T) { require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body) require.NotNil(t, res.Body) require.Equal(t, overrideID, res.Body.Data.OverrideId) - require.Equal(t, namespaceID, res.Body.Data.NamespaceId) require.Equal(t, identifier, res.Body.Data.Identifier) require.Equal(t, int64(limit), res.Body.Data.Limit) require.Equal(t, int64(duration), res.Body.Data.Duration) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/handler.go b/go/apps/api/routes/v2_ratelimit_get_override/handler.go index 2c29652e91..75982d52ca 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/handler.go @@ -153,11 +153,10 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { RequestId: s.RequestID(), }, Data: openapi.RatelimitOverride{ - OverrideId: override.ID, - NamespaceId: namespace.ID, - Limit: override.Limit, - Duration: override.Duration, - Identifier: override.Identifier, + OverrideId: override.ID, + Limit: override.Limit, + Duration: override.Duration, + Identifier: override.Identifier, }, }) } diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go index ad897413f0..caa97ab481 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go @@ -72,7 +72,6 @@ func TestListOverridesSuccessfully(t *testing.T) { require.NotNil(t, res.Body) require.Len(t, res.Body.Data, 1) require.Equal(t, overrideID, res.Body.Data[0].OverrideId) - require.Equal(t, namespaceID, res.Body.Data[0].NamespaceId) require.Equal(t, identifier, res.Body.Data[0].Identifier) require.Equal(t, int64(limit), res.Body.Data[0].Limit) require.Equal(t, int64(duration), res.Body.Data[0].Duration) @@ -89,7 +88,6 @@ func TestListOverridesSuccessfully(t *testing.T) { require.NotNil(t, res.Body) require.Len(t, res.Body.Data, 1) require.Equal(t, overrideID, res.Body.Data[0].OverrideId) - require.Equal(t, namespaceID, res.Body.Data[0].NamespaceId) require.Equal(t, identifier, res.Body.Data[0].Identifier) require.Equal(t, int64(limit), res.Body.Data[0].Limit) require.Equal(t, int64(duration), res.Body.Data[0].Duration) diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/IMPLEMENTATION_SUMMARY.md b/go/apps/api/routes/v2_ratelimit_list_overrides/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 2fe434ce58..0000000000 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,205 +0,0 @@ -# v2_ratelimit_list_overrides Implementation Summary - -## Overview -Successfully completed the implementation of the `v2_ratelimit_list_overrides` endpoint, including full database operations, OpenAPI specifications, handler logic, and comprehensive test coverage. - -## What Was Completed - -### 1. Database Operations -- **List Overrides**: Uses `ListRatelimitOverrides` query to fetch overrides by namespace -- **Namespace Lookup**: Support for both `FindRatelimitNamespaceByID` and `FindRatelimitNamespaceByName` -- **Workspace Filtering**: All queries properly filter by workspace ID for isolation -- **Soft Delete Awareness**: Database query excludes deleted overrides (deleted_at_m IS NULL) - -### 2. OpenAPI Specification -- Complete OpenAPI schema for `/v2/ratelimit.listOverrides` endpoint -- Defined request/response body structures with proper validation -- Support for both namespace ID and namespace name lookup -- Pagination support in response structure (cursor and limit fields) -- Comprehensive error response definitions - -### 3. Generated Types -- OpenAPI code generation produces proper Go structs: - - `V2RatelimitListOverridesRequestBody` - - `V2RatelimitListOverridesResponseBody` - - `RatelimitListOverridesResponseData` (slice of RatelimitOverride) -- Handles optional namespace fields correctly -- Proper pagination structure with HasMore boolean and Cursor pointer - -### 4. Handler Implementation -The handler (`handler.go`) provides complete functionality: - -#### Features Implemented: -- **Flexible Namespace Lookup**: Supports both `namespaceId` and `namespaceName` -- **Empty Results Handling**: Returns empty array with 200 status (not 404) when no overrides exist -- **Permission Checking**: RBAC with namespace-specific and wildcard permissions -- **Response Format**: Consistent pagination object and override data structure -- **Transaction Safety**: All database operations use read-only transactions -- **Input Validation**: - - Either namespace ID or name required - - OpenAPI schema validation - - Proper workspace validation - -#### Error Handling: -- 400: Bad request (validation errors, missing fields) -- 401: Unauthorized (invalid/missing auth) -- 403: Forbidden (insufficient permissions) - masked as 404 for security -- 404: Not found (namespace doesn't exist) - -### 5. Test Coverage -Comprehensive test suite covering all scenarios: - -#### Success Cases (`200_test.go`): -- List overrides by namespace ID -- List overrides by namespace name -- List empty namespace (no overrides) - returns empty array with 200 status -- Verify pagination object presence -- Verify override data accuracy - -#### Error Cases: -- **400_test.go**: Missing fields, auth issues, schema validation -- **401_test.go**: Invalid authentication tokens -- **403_test.go**: Cross-workspace access attempts (returns 404 for security) -- **404_test.go**: Non-existent namespaces (by ID and name) - -### 6. Key Technical Decisions - -#### Security Model: -- Cross-workspace access returns 404 instead of 403 for security -- Namespace-specific permission checking with fallback to wildcard -- Proper workspace isolation throughout the process - -#### Data Consistency: -- Database query excludes soft-deleted overrides -- Empty results return 200 status with empty array (not 404) -- Consistent response format with pagination metadata - -#### Permission Model: -- Supports both specific (`ratelimit.{namespaceId}.read_override`) and wildcard permissions -- Validates namespace ownership within the workspace -- Falls back gracefully between permission types - -### 7. Route Registration -- **Missing Registration**: Added import and registration in `routes/register.go` -- **RBAC Integration**: Added `ListOverrides` action to RBAC permissions system -- **Middleware Stack**: Uses standard middleware (auth, validation, logging, error handling) - -## Current Status: ✅ COMPLETE - -- ✅ Database queries implemented and tested -- ✅ OpenAPI specification complete -- ✅ Generated types working correctly -- ✅ Handler fully implemented with all features -- ✅ Comprehensive test coverage (100% pass rate) -- ✅ Error handling for all edge cases -- ✅ Security and validation measures in place -- ✅ Route registration completed -- ✅ RBAC permissions integrated -- ✅ Empty result handling implemented correctly - -## Usage Example - -```bash -curl -X POST https://api.unkey.dev/v2/ratelimit.listOverrides \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "namespaceName": "my-namespace" - }' -``` - -Or by namespace ID: - -```bash -curl -X POST https://api.unkey.dev/v2/ratelimit.listOverrides \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "namespaceId": "rlns_123456789" - }' -``` - -## Response Format - -### Success Response (200) -```json -{ - "data": [ - { - "overrideId": "rlmo_123456789", - "namespaceId": "rlns_123456789", - "identifier": "user_premium_*", - "limit": 1000, - "duration": 3600000 - } - ], - "pagination": { - "hasMore": false, - "cursor": null - }, - "meta": { - "requestId": "req_123456789" - } -} -``` - -### Empty Result Response (200) -```json -{ - "data": [], - "pagination": { - "hasMore": false, - "cursor": null - }, - "meta": { - "requestId": "req_123456789" - } -} -``` - -## Integration Notes - -The endpoint is fully integrated into the API: -- Registered in `routes/register.go` -- Uses standard middleware stack (auth, validation, logging, error handling) -- Follows established patterns from other v2 ratelimit endpoints -- Compatible with existing audit and monitoring systems - -## Technical Architecture - -### Request Flow: -1. **Authentication**: Root key verification and workspace validation -2. **Input Validation**: OpenAPI schema validation and custom validation -3. **Namespace Lookup**: Find namespace by ID or name within workspace -4. **Permission Check**: RBAC validation for read_override action -5. **Override Query**: Fetch all non-deleted overrides for the namespace -6. **Response Building**: Format override data with pagination metadata -7. **JSON Response**: Return structured response with metadata - -### Database Schema: -- Filters out soft-deleted overrides (deleted_at_m IS NULL) -- Orders results by created_at_m DESC -- Maintains workspace isolation at all levels -- Supports efficient namespace lookups by both ID and name - -### Security Features: -- Workspace isolation prevents cross-tenant access -- Permission-based access control with granular namespace permissions -- Error masking (404 instead of 403) to prevent information disclosure -- Consistent response format for both populated and empty results - -## Future Enhancements - -### Pagination Implementation -The response structure supports pagination but full implementation requires: -- Database query LIMIT and OFFSET parameters -- Cursor-based pagination logic -- HasMore calculation based on result count -- Cursor generation for next page requests - -### Performance Optimizations -- Index optimization for namespace + workspace queries -- Response caching for frequently accessed namespaces -- Bulk loading for multiple namespace requests - -All tests pass and the implementation is ready for production use. \ No newline at end of file diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go index 08b8aa33cb..e3e42a8da8 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go @@ -10,6 +10,7 @@ import ( "github.com/unkeyed/unkey/go/pkg/db" "github.com/unkeyed/unkey/go/pkg/fault" "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/rbac" "github.com/unkeyed/unkey/go/pkg/zen" ) @@ -49,7 +50,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } // Use the namespace field directly - it can be either name or ID - response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + namespace, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ WorkspaceID: auth.AuthorizedWorkspaceID, Namespace: req.Namespace, }) @@ -66,15 +67,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { ) } - namespace := db.RatelimitNamespace{ - ID: response.ID, - WorkspaceID: response.WorkspaceID, - Name: response.Name, - CreatedAtM: response.CreatedAtM, - UpdatedAtM: response.UpdatedAtM, - DeletedAtM: response.DeletedAtM, - } - if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { return fault.New("namespace not found", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), @@ -98,33 +90,43 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } + limit := ptr.SafeDeref(req.Limit, 50) + overrides, err := db.Query.ListRatelimitOverridesByNamespaceID(ctx, h.DB.RO(), db.ListRatelimitOverridesByNamespaceIDParams{ WorkspaceID: auth.AuthorizedWorkspaceID, NamespaceID: namespace.ID, + Limit: int32(limit) + 1, + CursorID: ptr.SafeDeref(req.Cursor, ""), }) if err != nil { return err } + hasMore := len(overrides) > limit + var cursor *string + if hasMore { + cursor = ptr.P(overrides[limit].ID) + overrides = overrides[:limit] + } + responseBody := Response{ Meta: openapi.Meta{ RequestId: s.RequestID(), }, Data: make([]openapi.RatelimitOverride, len(overrides)), Pagination: &openapi.Pagination{ - Cursor: nil, - HasMore: false, + Cursor: cursor, + HasMore: hasMore, }, } for i, override := range overrides { responseBody.Data[i] = openapi.RatelimitOverride{ - OverrideId: override.ID, - Duration: int64(override.Duration), - Identifier: override.Identifier, - NamespaceId: override.NamespaceID, - Limit: int64(override.Limit), + OverrideId: override.ID, + Duration: int64(override.Duration), + Identifier: override.Identifier, + Limit: int64(override.Limit), } } diff --git a/go/apps/api/routes/v2_ratelimit_set_override/handler.go b/go/apps/api/routes/v2_ratelimit_set_override/handler.go index 2a1905a8eb..7230580fc7 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/handler.go @@ -94,6 +94,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if err != nil { return "", err } + override, err := db.Query.FindRatelimitOverrideByIdentifier(ctx, tx, db.FindRatelimitOverrideByIdentifierParams{ WorkspaceID: auth.AuthorizedWorkspaceID, NamespaceID: namespace.ID, diff --git a/go/internal/services/keys/validation.go b/go/internal/services/keys/validation.go index ab62a8fb32..89c6d37f91 100644 --- a/go/internal/services/keys/validation.go +++ b/go/internal/services/keys/validation.go @@ -170,9 +170,9 @@ func (k *KeyVerifier) withRateLimits(ctx context.Context, specifiedLimits []open dbRl, exists := k.ratelimitConfigs[rl.Name] if !exists { - errorMsg := "ratelimit %q was requested but does not exist for key %q" + errorMsg := "ratelimit '%s' was requested but does not exist for key '%s'" if k.Key.IdentityID.Valid { - errorMsg += " nor identity: %q external ID: %q" + errorMsg += " nor identity: '%s' external ID: '%s'" } else { errorMsg += " and there is no identity connected." } diff --git a/go/pkg/clickhouse/client.go b/go/pkg/clickhouse/client.go index 2b1e213eb2..a7d4e8c162 100644 --- a/go/pkg/clickhouse/client.go +++ b/go/pkg/clickhouse/client.go @@ -57,7 +57,6 @@ type Config struct { // return fmt.Errorf("failed to initialize clickhouse: %w", err) // } func New(config Config) (*clickhouse, error) { - opts, err := ch.ParseDSN(config.URL) if err != nil { return nil, fault.Wrap(err, fault.Internal("parsing clickhouse DSN failed")) @@ -97,7 +96,7 @@ func New(config Config) (*clickhouse, error) { logger: config.Logger, requests: batch.New(batch.Config[schema.ApiRequestV1]{ - Name: "api requests", + Name: "api_requests", Drop: true, BatchSize: 10000, BufferSize: 100000, @@ -116,7 +115,7 @@ func New(config Config) (*clickhouse, error) { }), keyVerifications: batch.New[schema.KeyVerificationRequestV1]( batch.Config[schema.KeyVerificationRequestV1]{ - Name: "key verifications", + Name: "key_verifications", Drop: true, BatchSize: 10000, BufferSize: 100000, @@ -135,7 +134,7 @@ func New(config Config) (*clickhouse, error) { }), ratelimits: batch.New[schema.RatelimitRequestV1]( batch.Config[schema.RatelimitRequestV1]{ - Name: "rate limits", + Name: "ratelimits", Drop: true, BatchSize: 10000, BufferSize: 100000, @@ -154,10 +153,6 @@ func New(config Config) (*clickhouse, error) { }), } - // err = c.conn.Ping(context.Background()) - // if err != nil { - // return nil, fault.Wrap(err, fault.With("pinging clickhouse failed")) - // } return c, nil } diff --git a/go/pkg/codes/constants_gen.go b/go/pkg/codes/constants_gen.go index 4fce01b97d..70deeca842 100644 --- a/go/pkg/codes/constants_gen.go +++ b/go/pkg/codes/constants_gen.go @@ -6,20 +6,20 @@ type URN string // Error code constants for use in switch statements for exhaustive checking const ( -// ---------------- -// UserErrors -// ---------------- + // ---------------- + // UserErrors + // ---------------- -// BadRequest + // BadRequest // PermissionsQuerySyntaxError indicates a syntax or lexical error in verifyKey permissions query parsing. UserErrorsBadRequestPermissionsQuerySyntaxError URN = "err:user:bad_request:permissions_query_syntax_error" -// ---------------- -// UnkeyAuthErrors -// ---------------- + // ---------------- + // UnkeyAuthErrors + // ---------------- -// Authentication + // Authentication // Missing indicates authentication credentials were not provided. UnkeyAuthErrorsAuthenticationMissing URN = "err:unkey:authentication:missing" @@ -28,7 +28,7 @@ const ( // KeyNotFound indicates the authentication key was not found. UnkeyAuthErrorsAuthenticationKeyNotFound URN = "err:unkey:authentication:key_not_found" -// Authorization + // Authorization // InsufficientPermissions indicates the authenticated entity lacks // sufficient permissions for the requested operation. @@ -40,92 +40,91 @@ const ( // WorkspaceDisabled indicates the associated workspace is disabled. UnkeyAuthErrorsAuthorizationWorkspaceDisabled URN = "err:unkey:authorization:workspace_disabled" -// ---------------- -// UnkeyDataErrors -// ---------------- + // ---------------- + // UnkeyDataErrors + // ---------------- -// Key + // Key // NotFound indicates the requested key was not found. UnkeyDataErrorsKeyNotFound URN = "err:unkey:data:key_not_found" -// Workspace + // Workspace // NotFound indicates the requested workspace was not found. UnkeyDataErrorsWorkspaceNotFound URN = "err:unkey:data:workspace_not_found" -// Api + // Api // NotFound indicates the requested API was not found. UnkeyDataErrorsApiNotFound URN = "err:unkey:data:api_not_found" -// Permission + // Permission // Duplicate indicates the requested permission already exists. UnkeyDataErrorsPermissionDuplicate URN = "err:unkey:data:permission_already_exists" // NotFound indicates the requested permission was not found. UnkeyDataErrorsPermissionNotFound URN = "err:unkey:data:permission_not_found" -// Role + // Role // Duplicate indicates the requested role already exists. UnkeyDataErrorsRoleDuplicate URN = "err:unkey:data:role_already_exists" // NotFound indicates the requested role was not found. UnkeyDataErrorsRoleNotFound URN = "err:unkey:data:role_not_found" -// KeyAuth + // KeyAuth // NotFound indicates the requested key authentication was not found. UnkeyDataErrorsKeyAuthNotFound URN = "err:unkey:data:key_auth_not_found" -// RatelimitNamespace + // RatelimitNamespace // NotFound indicates the requested rate limit namespace was not found. UnkeyDataErrorsRatelimitNamespaceNotFound URN = "err:unkey:data:ratelimit_namespace_not_found" -// RatelimitOverride + // RatelimitOverride // NotFound indicates the requested rate limit override was not found. UnkeyDataErrorsRatelimitOverrideNotFound URN = "err:unkey:data:ratelimit_override_not_found" -// Identity + // Identity // NotFound indicates the requested identity was not found. UnkeyDataErrorsIdentityNotFound URN = "err:unkey:data:identity_not_found" // Duplicate indicates the requested identity already exists. UnkeyDataErrorsIdentityDuplicate URN = "err:unkey:data:identity_already_exists" -// AuditLog + // AuditLog // NotFound indicates the requested audit log was not found. UnkeyDataErrorsAuditLogNotFound URN = "err:unkey:data:audit_log_not_found" -// ---------------- -// UnkeyAppErrors -// ---------------- + // ---------------- + // UnkeyAppErrors + // ---------------- -// Internal + // Internal // UnexpectedError represents an unhandled or unexpected error condition. UnkeyAppErrorsInternalUnexpectedError URN = "err:unkey:application:unexpected_error" // ServiceUnavailable indicates a service is temporarily unavailable. UnkeyAppErrorsInternalServiceUnavailable URN = "err:unkey:application:service_unavailable" -// Validation + // Validation // InvalidInput indicates a client provided input that failed validation. UnkeyAppErrorsValidationInvalidInput URN = "err:unkey:application:invalid_input" // AssertionFailed indicates a runtime assertion or invariant check failed. UnkeyAppErrorsValidationAssertionFailed URN = "err:unkey:application:assertion_failed" -// Protection + // Protection // ProtectedResource indicates an attempt to modify a protected resource. UnkeyAppErrorsProtectionProtectedResource URN = "err:unkey:application:protected_resource" -// Precondition + // Precondition // PreconditionFailed indicates a precondition check failed. UnkeyAppErrorsPreconditionPreconditionFailed URN = "err:unkey:application:precondition_failed" - ) diff --git a/go/pkg/db/permission_list.sql_generated.go b/go/pkg/db/permission_list.sql_generated.go index e41e44d30c..aabfce89f0 100644 --- a/go/pkg/db/permission_list.sql_generated.go +++ b/go/pkg/db/permission_list.sql_generated.go @@ -13,14 +13,15 @@ const listPermissions = `-- name: ListPermissions :many SELECT p.id, p.workspace_id, p.name, p.slug, p.description, p.created_at_m, p.updated_at_m FROM permissions p WHERE p.workspace_id = ? - AND p.id > ? + AND p.id >= ? ORDER BY p.id -LIMIT 101 +LIMIT ? ` type ListPermissionsParams struct { WorkspaceID string `db:"workspace_id"` IDCursor string `db:"id_cursor"` + Limit int32 `db:"limit"` } // ListPermissions @@ -28,11 +29,11 @@ type ListPermissionsParams struct { // SELECT p.id, p.workspace_id, p.name, p.slug, p.description, p.created_at_m, p.updated_at_m // FROM permissions p // WHERE p.workspace_id = ? -// AND p.id > ? +// AND p.id >= ? // ORDER BY p.id -// LIMIT 101 +// LIMIT ? func (q *Queries) ListPermissions(ctx context.Context, db DBTX, arg ListPermissionsParams) ([]Permission, error) { - rows, err := db.QueryContext(ctx, listPermissions, arg.WorkspaceID, arg.IDCursor) + rows, err := db.QueryContext(ctx, listPermissions, arg.WorkspaceID, arg.IDCursor, arg.Limit) if err != nil { return nil, err } diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index d63aa37fb1..b7d95ae6ff 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -1127,9 +1127,9 @@ type Querier interface { // SELECT p.id, p.workspace_id, p.name, p.slug, p.description, p.created_at_m, p.updated_at_m // FROM permissions p // WHERE p.workspace_id = ? - // AND p.id > ? + // AND p.id >= ? // ORDER BY p.id - // LIMIT 101 + // LIMIT ? ListPermissions(ctx context.Context, db DBTX, arg ListPermissionsParams) ([]Permission, error) //ListPermissionsByKeyID // @@ -1165,8 +1165,12 @@ type Querier interface { // // SELECT id, workspace_id, namespace_id, identifier, `limit`, duration, async, sharding, created_at_m, updated_at_m, deleted_at_m FROM ratelimit_overrides // WHERE - // workspace_id = ? - // AND namespace_id = ? + // workspace_id = ? + // AND namespace_id = ? + // AND deleted_at_m IS NULL + // AND id >= ? + // ORDER BY id ASC + // LIMIT ? ListRatelimitOverridesByNamespaceID(ctx context.Context, db DBTX, arg ListRatelimitOverridesByNamespaceIDParams) ([]RatelimitOverride, error) //ListRatelimitsByKeyID // @@ -1211,9 +1215,9 @@ type Querier interface { // ) as permissions // FROM roles r // WHERE r.workspace_id = ? - // AND r.id > ? + // AND r.id >= ? // ORDER BY r.id - // LIMIT 101 + // LIMIT ? ListRoles(ctx context.Context, db DBTX, arg ListRolesParams) ([]ListRolesRow, error) //ListRolesByKeyID // diff --git a/go/pkg/db/queries/permission_list.sql b/go/pkg/db/queries/permission_list.sql index 3d19bcfd5e..f3549741f6 100644 --- a/go/pkg/db/queries/permission_list.sql +++ b/go/pkg/db/queries/permission_list.sql @@ -2,6 +2,6 @@ SELECT p.* FROM permissions p WHERE p.workspace_id = sqlc.arg(workspace_id) - AND p.id > sqlc.arg(id_cursor) + AND p.id >= sqlc.arg(id_cursor) ORDER BY p.id -LIMIT 101; \ No newline at end of file +LIMIT ?; diff --git a/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql b/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql index e4525c440f..65d0419e9e 100644 --- a/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql +++ b/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql @@ -1,5 +1,9 @@ -- name: ListRatelimitOverridesByNamespaceID :many SELECT * FROM ratelimit_overrides WHERE - workspace_id = sqlc.arg(workspace_id) - AND namespace_id = sqlc.arg(namespace_id); +workspace_id = sqlc.arg(workspace_id) +AND namespace_id = sqlc.arg(namespace_id) +AND deleted_at_m IS NULL +AND id >= sqlc.arg(cursor_id) +ORDER BY id ASC +LIMIT ?; diff --git a/go/pkg/db/queries/role_list.sql b/go/pkg/db/queries/role_list.sql index 1069fdd3d8..fff4823472 100644 --- a/go/pkg/db/queries/role_list.sql +++ b/go/pkg/db/queries/role_list.sql @@ -16,6 +16,6 @@ SELECT r.*, COALESCE( ) as permissions FROM roles r WHERE r.workspace_id = sqlc.arg(workspace_id) -AND r.id > sqlc.arg(id_cursor) +AND r.id >= sqlc.arg(id_cursor) ORDER BY r.id -LIMIT 101; +LIMIT ?; diff --git a/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go b/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go index 750cd837f7..703b91cec1 100644 --- a/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go +++ b/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go @@ -12,23 +12,38 @@ import ( const listRatelimitOverridesByNamespaceID = `-- name: ListRatelimitOverridesByNamespaceID :many SELECT id, workspace_id, namespace_id, identifier, ` + "`" + `limit` + "`" + `, duration, async, sharding, created_at_m, updated_at_m, deleted_at_m FROM ratelimit_overrides WHERE - workspace_id = ? - AND namespace_id = ? +workspace_id = ? +AND namespace_id = ? +AND deleted_at_m IS NULL +AND id >= ? +ORDER BY id ASC +LIMIT ? ` type ListRatelimitOverridesByNamespaceIDParams struct { WorkspaceID string `db:"workspace_id"` NamespaceID string `db:"namespace_id"` + CursorID string `db:"cursor_id"` + Limit int32 `db:"limit"` } // ListRatelimitOverridesByNamespaceID // // SELECT id, workspace_id, namespace_id, identifier, `limit`, duration, async, sharding, created_at_m, updated_at_m, deleted_at_m FROM ratelimit_overrides // WHERE -// workspace_id = ? -// AND namespace_id = ? +// workspace_id = ? +// AND namespace_id = ? +// AND deleted_at_m IS NULL +// AND id >= ? +// ORDER BY id ASC +// LIMIT ? func (q *Queries) ListRatelimitOverridesByNamespaceID(ctx context.Context, db DBTX, arg ListRatelimitOverridesByNamespaceIDParams) ([]RatelimitOverride, error) { - rows, err := db.QueryContext(ctx, listRatelimitOverridesByNamespaceID, arg.WorkspaceID, arg.NamespaceID) + rows, err := db.QueryContext(ctx, listRatelimitOverridesByNamespaceID, + arg.WorkspaceID, + arg.NamespaceID, + arg.CursorID, + arg.Limit, + ) if err != nil { return nil, err } diff --git a/go/pkg/db/role_list.sql_generated.go b/go/pkg/db/role_list.sql_generated.go index ef516e3fff..2292c8119f 100644 --- a/go/pkg/db/role_list.sql_generated.go +++ b/go/pkg/db/role_list.sql_generated.go @@ -28,14 +28,15 @@ SELECT r.id, r.workspace_id, r.name, r.description, r.created_at_m, r.updated_at ) as permissions FROM roles r WHERE r.workspace_id = ? -AND r.id > ? +AND r.id >= ? ORDER BY r.id -LIMIT 101 +LIMIT ? ` type ListRolesParams struct { WorkspaceID string `db:"workspace_id"` IDCursor string `db:"id_cursor"` + Limit int32 `db:"limit"` } type ListRolesRow struct { @@ -67,11 +68,11 @@ type ListRolesRow struct { // ) as permissions // FROM roles r // WHERE r.workspace_id = ? -// AND r.id > ? +// AND r.id >= ? // ORDER BY r.id -// LIMIT 101 +// LIMIT ? func (q *Queries) ListRoles(ctx context.Context, db DBTX, arg ListRolesParams) ([]ListRolesRow, error) { - rows, err := db.QueryContext(ctx, listRoles, arg.WorkspaceID, arg.IDCursor) + rows, err := db.QueryContext(ctx, listRoles, arg.WorkspaceID, arg.IDCursor, arg.Limit) if err != nil { return nil, err } diff --git a/go/pkg/zen/middleware_errors.go b/go/pkg/zen/middleware_errors.go index e9b56d8a54..7dd1ee1261 100644 --- a/go/pkg/zen/middleware_errors.go +++ b/go/pkg/zen/middleware_errors.go @@ -116,6 +116,8 @@ func WithErrorHandling(logger logging.Logger) Middleware { Status: http.StatusForbidden, }, }) + + // Duplicate errors case codes.UnkeyDataErrorsIdentityDuplicate, codes.UnkeyDataErrorsRoleDuplicate, codes.UnkeyDataErrorsPermissionDuplicate: @@ -124,12 +126,13 @@ func WithErrorHandling(logger logging.Logger) Middleware { RequestId: s.RequestID(), }, Error: openapi.BaseError{ - Title: "Duplicate Identity", + Title: "Conflicting Resource", Type: code.DocsURL(), Detail: fault.UserFacingMessage(err), Status: http.StatusConflict, }, }) + // Protected Resource case codes.UnkeyAppErrorsProtectionProtectedResource: return s.JSON(http.StatusPreconditionFailed, openapi.PreconditionFailedErrorResponse{