From 1f8a42e54822142132270df1aefa768f4626613f Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Wed, 22 Jul 2026 01:23:15 +0800 Subject: [PATCH] fix: enforce role authority for user management (#616) Centralize role-authority enforcement across user management, role management, configuration import, SCIM, GDPR, and MFA mutations. Add regression coverage for delegated custom roles and protect higher-privilege accounts from reset, deletion, or takeover. --- apps/api/src/openapi.ar.yaml | 163 +++--- apps/api/src/openapi.de.yaml | 163 +++--- apps/api/src/openapi.es.yaml | 163 +++--- apps/api/src/openapi.fr.yaml | 163 +++--- apps/api/src/openapi.hi.yaml | 163 +++--- apps/api/src/openapi.id.yaml | 163 +++--- apps/api/src/openapi.it.yaml | 163 +++--- apps/api/src/openapi.ja.yaml | 158 ++--- apps/api/src/openapi.ko.yaml | 163 +++--- apps/api/src/openapi.nl.yaml | 158 ++--- apps/api/src/openapi.pl.yaml | 163 +++--- apps/api/src/openapi.pt-BR.yaml | 163 +++--- apps/api/src/openapi.ru.yaml | 163 +++--- apps/api/src/openapi.sv.yaml | 163 +++--- apps/api/src/openapi.th.yaml | 158 ++--- apps/api/src/openapi.tr.yaml | 163 +++--- apps/api/src/openapi.uk.yaml | 163 +++--- apps/api/src/openapi.vi.yaml | 163 +++--- apps/api/src/openapi.yaml | 70 ++- apps/api/src/openapi.zh-CN.yaml | 158 ++--- apps/api/src/openapi.zh-TW.yaml | 158 ++--- apps/api/src/permissions.ts | 307 ++++++++-- apps/api/src/plugins/auth.ts | 45 +- apps/api/src/plugins/mfa.ts | 28 +- apps/api/src/routes/enterprise/config.ts | 66 ++- apps/api/src/routes/enterprise/gdpr.ts | 29 +- apps/api/src/routes/enterprise/scim.ts | 116 +++- apps/api/src/routes/roles.ts | 79 ++- apps/docs/api/rest.md | 26 +- apps/docs/guide/scim.md | 14 +- apps/docs/guide/users-roles.md | 18 +- .../platform/config-export-import.test.ts | 225 +++++++- .../platform/custom-roles-edge-cases.test.ts | 399 ++++++++++++- .../platform/gdpr-lifecycle.test.ts | 198 ++++++- .../platform/role-authority.test.ts | 542 ++++++++++++++++++ tests/integration/platform/scim.test.ts | 453 ++++++++++++++- 36 files changed, 4266 insertions(+), 1584 deletions(-) create mode 100644 tests/integration/platform/role-authority.test.ts diff --git a/apps/api/src/openapi.ar.yaml b/apps/api/src/openapi.ar.yaml index 2a0b25c9..b5fb904b 100644 --- a/apps/api/src/openapi.ar.yaml +++ b/apps/api/src/openapi.ar.yaml @@ -5817,7 +5817,8 @@ paths: tags: - Auth summary: إنشاء مستخدم - description: يتطلب إذن users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5888,7 +5889,8 @@ paths: tags: - Auth summary: تحديث مستخدم - description: يتطلب إذن users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5943,7 +5945,8 @@ paths: tags: - Auth summary: حذف مستخدم - description: يتطلب إذن users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5981,7 +5984,8 @@ paths: tags: - Auth summary: إعادة تعيين كلمة مرور المستخدم - description: يتطلب إذن users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7159,9 +7163,10 @@ paths: - Roles summary: إنشاء دور مخصص description: | - إنشاء دور مخصص جديد بأذونات محددة. يجب أن تتراوح أسماء الأدوار - بين 2 و30 حرفًا، أحرف صغيرة وأرقام مع شرطات وشرطات سفلية. - يتطلب إذن security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7191,7 +7196,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7244,8 +7250,10 @@ paths: - Roles summary: تحديث دور مخصص description: | - تحديث اسم أو وصف أو أذونات دور مخصص. لا يمكن تعديل الأدوار - المدمجة. يتطلب إذن security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7319,9 +7327,10 @@ paths: - Roles summary: حذف دور مخصص description: | - حذف دور مخصص. يُعاد تعيين المستخدمين المسندين إلى هذا الدور إلى - دور "user" الافتراضي. لا يمكن حذف الأدوار المدمجة. يتطلب - إذن security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12111,6 +12120,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17798,7 +17861,8 @@ paths: tags: - Auth summary: إعادة تعيين MFA للمستخدم - description: إعادة تعيين المسؤول لتسجيل MFA الخاص بمستخدم. يتطلب إذن users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18125,7 +18189,8 @@ paths: tags: - Enterprise summary: إنشاء رمز SCIM - description: إنشاء رمز bearer لـ SCIM. يتطلب إذن users:manage وميزة SCIM المؤسسية. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18138,7 +18203,8 @@ paths: tags: - Enterprise summary: إبطال رمز SCIM - description: إبطال رمز bearer الحالي لـ SCIM. يتطلب إذن users:manage وميزة SCIM المؤسسية. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18345,7 +18411,8 @@ paths: tags: - Enterprise summary: استيراد الإعدادات - description: استيراد إعدادات المثيل والأدوار المخصصة والفرق. يتطلب إذن system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18589,7 +18656,8 @@ paths: tags: - Enterprise summary: إزالة بيانات المستخدم - description: إزالة بيانات مستخدم نهائيًا بعد تأكيد صريح. يتطلب إذن compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18603,6 +18671,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18611,7 +18681,8 @@ paths: tags: - Enterprise summary: إزالة بيانات الفريق - description: إزالة بيانات فريق نهائيًا بعد تأكيد صريح. يتطلب إذن compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18625,6 +18696,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19550,34 +19623,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: dbebc3f700af - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 08112341b1ef paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: b990ac968470 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 08112341b1ef paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: e670394a1970 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 08112341b1ef paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: a64e248daefa - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 08112341b1ef paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19742,26 +19799,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 111394d18cf0 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 076bd1d067f7 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 05460e86f14c - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 50498644b071 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 53e75eda78a1 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: bb03c35161f3 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21342,10 +21387,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 8f1c3a860423 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 5bcdb74d0a67 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21470,18 +21511,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: b8084ae55921 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 2684cfe1d6a3 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 1d88f493c962 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 62af942e9681 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21606,10 +21639,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: cb053b845a9d - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: d1594a0a169a paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21718,18 +21747,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 481a56ba7274 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 87f78207dd7d paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 8d91f3a6738f - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: fe1b8a56795d paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.de.yaml b/apps/api/src/openapi.de.yaml index 4c074820..c0cf5a7a 100644 --- a/apps/api/src/openapi.de.yaml +++ b/apps/api/src/openapi.de.yaml @@ -5814,7 +5814,8 @@ paths: tags: - Auth summary: Benutzer erstellen - description: Erfordert die Berechtigung users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5885,7 +5886,8 @@ paths: tags: - Auth summary: Benutzer aktualisieren - description: Erfordert die Berechtigung users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5940,7 +5942,8 @@ paths: tags: - Auth summary: Benutzer löschen - description: Erfordert die Berechtigung users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5978,7 +5981,8 @@ paths: tags: - Auth summary: Benutzerpasswort zurücksetzen - description: Erfordert die Berechtigung users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7156,9 +7160,10 @@ paths: - Roles summary: Eine benutzerdefinierte Rolle erstellen description: | - Eine neue benutzerdefinierte Rolle mit angegebenen Berechtigungen erstellen. Rollennamen müssen - 2-30 Zeichen lang sein, alphanumerisch klein geschrieben mit Bindestrichen und Unterstrichen. - Erfordert die Berechtigung security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7188,7 +7193,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7241,8 +7247,10 @@ paths: - Roles summary: Eine benutzerdefinierte Rolle aktualisieren description: | - Name, Beschreibung oder Berechtigungen einer benutzerdefinierten Rolle aktualisieren. Integrierte - Rollen können nicht geändert werden. Erfordert die Berechtigung security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7316,9 +7324,10 @@ paths: - Roles summary: Eine benutzerdefinierte Rolle löschen description: | - Eine benutzerdefinierte Rolle löschen. Benutzer, die dieser Rolle zugewiesen sind, werden der - Standardrolle "user" neu zugewiesen. Integrierte Rollen können nicht gelöscht werden. Erfordert - die Berechtigung security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12108,6 +12117,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17795,7 +17858,8 @@ paths: tags: - Auth summary: Benutzer-MFA zurücksetzen - description: Admin-Zurücksetzung der MFA-Einrichtung eines Benutzers. Erfordert die Berechtigung users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18122,7 +18186,8 @@ paths: tags: - Enterprise summary: SCIM-Token erstellen - description: Ein SCIM-Bearer-Token erzeugen. Erfordert die Berechtigung users:manage und das Enterprise-SCIM-Feature. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18135,7 +18200,8 @@ paths: tags: - Enterprise summary: SCIM-Token widerrufen - description: Das aktuelle SCIM-Bearer-Token widerrufen. Erfordert die Berechtigung users:manage und das Enterprise-SCIM-Feature. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18342,7 +18408,8 @@ paths: tags: - Enterprise summary: Konfiguration importieren - description: Instanzkonfiguration, benutzerdefinierte Rollen und Teams importieren. Erfordert die Berechtigung system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18586,7 +18653,8 @@ paths: tags: - Enterprise summary: Benutzerdaten löschen - description: Die Daten eines Benutzers nach ausdrücklicher Bestätigung dauerhaft löschen. Erfordert die Berechtigung compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18600,6 +18668,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18608,7 +18678,8 @@ paths: tags: - Enterprise summary: Team-Daten löschen - description: Die Daten eines Teams nach ausdrücklicher Bestätigung dauerhaft löschen. Erfordert die Berechtigung compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18622,6 +18693,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19547,34 +19620,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: a50b5abbf09c - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 17a3197fcd5f paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 3223a0084321 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 17a3197fcd5f paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 814fdc6d6f74 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 17a3197fcd5f paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 3fc6203d6a2a - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 17a3197fcd5f paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19739,26 +19796,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: '335249490147' - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 6add64d3b1d3 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 57d541a39157 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 753658c5dd9f paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 302e196acd18 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 808a946370bb paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21339,10 +21384,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 9ffd3f0cd0d6 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 4bbf458b3ad8 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21467,18 +21508,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 39a1e046e5d9 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 13260683d257 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: eb94dc790434 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: c0d216991d8d paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21603,10 +21636,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 690fb30ee112 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 2bebc32c01ec paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21715,18 +21744,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 01174755ccf1 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 9514fa26e33f paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: b55808e9bacd - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 29f42c1c9b5f paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.es.yaml b/apps/api/src/openapi.es.yaml index 85d025a8..afb24674 100644 --- a/apps/api/src/openapi.es.yaml +++ b/apps/api/src/openapi.es.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Crear usuario - description: Requiere el permiso users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Actualizar usuario - description: Requiere el permiso users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Eliminar usuario - description: Requiere el permiso users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Restablecer la contraseña de un usuario - description: Requiere el permiso users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Crear un rol personalizado description: | - Crea un nuevo rol personalizado con los permisos indicados. Los nombres de rol deben tener - 2-30 caracteres, alfanuméricos en minúscula con guiones y guiones bajos. - Requiere el permiso security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Actualizar un rol personalizado description: | - Actualiza el nombre, la descripción o los permisos de un rol personalizado. Los roles - integrados no pueden modificarse. Requiere el permiso security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Eliminar un rol personalizado description: | - Elimina un rol personalizado. Los usuarios asignados a este rol se reasignan al - rol "user" predeterminado. Los roles integrados no pueden eliminarse. Requiere el - permiso security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17799,7 +17862,8 @@ paths: tags: - Auth summary: Restablecer MFA de un usuario - description: Restablecimiento por parte del administrador del registro de MFA de un usuario. Requiere el permiso users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18126,7 +18190,8 @@ paths: tags: - Enterprise summary: Crear token SCIM - description: Genera un token bearer de SCIM. Requiere el permiso users:manage y la función SCIM de enterprise. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18139,7 +18204,8 @@ paths: tags: - Enterprise summary: Revocar token SCIM - description: Revoca el token bearer de SCIM actual. Requiere el permiso users:manage y la función SCIM de enterprise. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18346,7 +18412,8 @@ paths: tags: - Enterprise summary: Importar configuración - description: Importa la configuración de instancia, los roles personalizados y los equipos. Requiere el permiso system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18590,7 +18657,8 @@ paths: tags: - Enterprise summary: Purgar datos de usuario - description: Purga permanentemente los datos de un usuario tras una confirmación explícita. Requiere el permiso compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18604,6 +18672,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18612,7 +18682,8 @@ paths: tags: - Enterprise summary: Purgar datos de equipo - description: Purga permanentemente los datos de un equipo tras una confirmación explícita. Requiere el permiso compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18626,6 +18697,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19551,34 +19624,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 190aba76b8a0 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1b7ec2432f9a paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 5738ed1df6b5 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1b7ec2432f9a paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: b3f9f6397026 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1b7ec2432f9a paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 0d30f5a09bc2 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1b7ec2432f9a paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19743,26 +19800,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 87161c203b8c - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: e2a9e8bff8c3 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 62fd08935921 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: fc52be4ecbdb paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: b6c2c533f892 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 9b6f9d2987be paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21343,10 +21388,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 9e838aa8213d - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: dede2a0c565e paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21471,18 +21512,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: feb56ea65892 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 09e4cae86bed paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 0c003b3c47d6 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 9ec5d11831a2 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21607,10 +21640,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: b6274c66f0c5 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 9bb010abd880 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21719,18 +21748,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 78152a7612a8 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: ea57c2a0a988 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: a0432aa852fd - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 6311ae255610 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.fr.yaml b/apps/api/src/openapi.fr.yaml index d2dc9bb7..487d81d0 100644 --- a/apps/api/src/openapi.fr.yaml +++ b/apps/api/src/openapi.fr.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Créer un utilisateur - description: Requiert la permission users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Mettre à jour un utilisateur - description: Requiert la permission users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Supprimer un utilisateur - description: Requiert la permission users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Réinitialiser le mot de passe d'un utilisateur - description: Requiert la permission users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7140,9 +7144,10 @@ paths: - Roles summary: Créer un rôle personnalisé description: | - Crée un nouveau rôle personnalisé avec les permissions spécifiées. Les noms de rôles doivent comporter - 2 à 30 caractères, alphanumériques minuscules avec tirets et traits de soulignement. - Requiert la permission security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7172,7 +7177,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7225,8 +7231,10 @@ paths: - Roles summary: Mettre à jour un rôle personnalisé description: | - Met à jour le nom, la description ou les permissions d'un rôle personnalisé. Les rôles - intégrés ne peuvent pas être modifiés. Requiert la permission security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7300,9 +7308,10 @@ paths: - Roles summary: Supprimer un rôle personnalisé description: | - Supprime un rôle personnalisé. Les utilisateurs affectés à ce rôle sont réaffectés au - rôle « user » par défaut. Les rôles intégrés ne peuvent pas être supprimés. Requiert - la permission security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12092,6 +12101,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17779,7 +17842,8 @@ paths: tags: - Auth summary: Réinitialiser la MFA d'un utilisateur - description: Réinitialisation administrative de l'inscription MFA d'un utilisateur. Requiert la permission users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18106,7 +18170,8 @@ paths: tags: - Enterprise summary: Créer un jeton SCIM - description: Génère un jeton bearer SCIM. Requiert la permission users:manage et la fonctionnalité enterprise SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18119,7 +18184,8 @@ paths: tags: - Enterprise summary: Révoquer un jeton SCIM - description: Révoque le jeton bearer SCIM courant. Requiert la permission users:manage et la fonctionnalité enterprise SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18326,7 +18392,8 @@ paths: tags: - Enterprise summary: Importer la configuration - description: Importe la configuration d'instance, les rôles personnalisés et les équipes. Requiert la permission system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18570,7 +18637,8 @@ paths: tags: - Enterprise summary: Purger les données d'un utilisateur - description: Purge définitivement les données d'un utilisateur après confirmation explicite. Requiert la permission compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18584,6 +18652,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18592,7 +18662,8 @@ paths: tags: - Enterprise summary: Purger les données d'une équipe - description: Purge définitivement les données d'une équipe après confirmation explicite. Requiert la permission compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18606,6 +18677,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19531,34 +19604,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 5888e06e5678 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d057ef406f2c paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 6166bce1d5c6 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d057ef406f2c paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: eb663d54b308 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d057ef406f2c paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 221df741b0ce - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d057ef406f2c paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19723,26 +19780,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: d644554d610b - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: a7d6584dc3d5 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: ae160427c13b - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 138ee155ebb1 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 72a9589fae5f - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 607d7dc1e3af paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21323,10 +21368,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: bc0e937f2197 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 85beaaa25359 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21451,18 +21492,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 01aa1e5e9eb6 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: ccc76563e1e7 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 8ef1eb6bda3d - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 02fae18f5733 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21587,10 +21620,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: a40e966d8e6e - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 1e3c37f1cb1a paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21699,18 +21728,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: e4a7d2553c6d - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 1d71c67324ed paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 2ae9f73c8b00 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 6a8f8f527033 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.hi.yaml b/apps/api/src/openapi.hi.yaml index 7c05584f..7b13fe36 100644 --- a/apps/api/src/openapi.hi.yaml +++ b/apps/api/src/openapi.hi.yaml @@ -5816,7 +5816,8 @@ paths: tags: - Auth summary: user बनाएं - description: users:manage permission आवश्यक है। + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5887,7 +5888,8 @@ paths: tags: - Auth summary: user अपडेट करें - description: users:manage permission आवश्यक है। + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5942,7 +5944,8 @@ paths: tags: - Auth summary: user हटाएं - description: users:manage permission आवश्यक है। + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5980,7 +5983,8 @@ paths: tags: - Auth summary: user password रीसेट करें - description: users:manage permission आवश्यक है। + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7158,9 +7162,10 @@ paths: - Roles summary: एक कस्टम role बनाएं description: | - निर्दिष्ट permissions के साथ एक नया कस्टम role बनाएं। Role नाम - 2-30 वर्ण, हाइफ़न और अंडरस्कोर के साथ लोअरकेस अल्फ़ान्यूमेरिक होने चाहिए। - security:manage permission आवश्यक है। + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7190,7 +7195,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7243,8 +7249,10 @@ paths: - Roles summary: एक कस्टम role अपडेट करें description: | - एक कस्टम role का नाम, description, या permissions अपडेट करें। बिल्ट-इन - roles संशोधित नहीं किए जा सकते। security:manage permission आवश्यक है। + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7318,9 +7326,10 @@ paths: - Roles summary: एक कस्टम role हटाएं description: | - एक कस्टम role हटाएं। इस role को असाइन किए गए Users को - डिफ़ॉल्ट "user" role में पुन: असाइन किया जाता है। बिल्ट-इन roles हटाए नहीं जा सकते। इसके लिए - security:manage permission आवश्यक है। + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12110,6 +12119,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17794,7 +17857,8 @@ paths: tags: - Auth summary: user MFA रीसेट करें - description: एक user के MFA नामांकन का Admin रीसेट। users:manage permission आवश्यक है। + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18121,7 +18185,8 @@ paths: tags: - Enterprise summary: SCIM token बनाएं - description: एक SCIM bearer token जनरेट करें। users:manage permission और enterprise SCIM feature आवश्यक है। + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18134,7 +18199,8 @@ paths: tags: - Enterprise summary: SCIM token रद्द करें - description: वर्तमान SCIM bearer token रद्द करें। users:manage permission और enterprise SCIM feature आवश्यक है। + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18341,7 +18407,8 @@ paths: tags: - Enterprise summary: configuration आयात करें - description: instance configuration, कस्टम roles, और teams आयात करें। system:health permission आवश्यक है। + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18585,7 +18652,8 @@ paths: tags: - Enterprise summary: user data पर्ज करें - description: स्पष्ट पुष्टि के बाद एक user का data स्थायी रूप से पर्ज करें। compliance:manage permission आवश्यक है। + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18599,6 +18667,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18607,7 +18677,8 @@ paths: tags: - Enterprise summary: team data पर्ज करें - description: स्पष्ट पुष्टि के बाद एक team का data स्थायी रूप से पर्ज करें। compliance:manage permission आवश्यक है। + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18621,6 +18692,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19546,34 +19619,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: ee4fc9fef8a4 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ca30c0150768 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: a80d88e0b5d2 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ca30c0150768 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 83e2cc37a32d - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ca30c0150768 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: b42e554ee28d - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ca30c0150768 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19738,26 +19795,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 20b639a2c3dc - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 52e2d30328f9 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: f823846ad3b6 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: b86914cd517d paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: f038a74fc497 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 25422003bd1a paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21338,10 +21383,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 885f22649f10 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 63b464cfeb05 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21466,18 +21507,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: a35ff7a09ebc - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 02b4ba77a723 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 632088aed2e8 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 4365e54244b1 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21602,10 +21635,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: a35b2c408440 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 7298ab6578d7 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21714,18 +21743,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: b1d2bfde9537 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 40ae1b27e4ed paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: a11ae49bffa6 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 7192acd5442a paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.id.yaml b/apps/api/src/openapi.id.yaml index a9f23c6e..8121f29d 100644 --- a/apps/api/src/openapi.id.yaml +++ b/apps/api/src/openapi.id.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Buat pengguna - description: Memerlukan permission users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Perbarui pengguna - description: Memerlukan permission users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Hapus pengguna - description: Memerlukan permission users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Reset password pengguna - description: Memerlukan permission users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Buat role kustom description: | - Buat role kustom baru dengan permission yang ditentukan. Nama role harus - 2-30 karakter, alfanumerik huruf kecil dengan tanda hubung dan garis bawah. - Memerlukan permission security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Perbarui role kustom description: | - Perbarui nama, deskripsi, atau permission dari role kustom. Role bawaan - tidak dapat diubah. Memerlukan permission security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Hapus role kustom description: | - Hapus role kustom. Pengguna yang ditetapkan ke role ini dipindahkan ke - role "user" default. Role bawaan tidak dapat dihapus. Memerlukan - permission security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17799,7 +17862,8 @@ paths: tags: - Auth summary: Reset MFA pengguna - description: Reset pendaftaran MFA pengguna oleh admin. Memerlukan permission users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18126,7 +18190,8 @@ paths: tags: - Enterprise summary: Buat token SCIM - description: Buat bearer token SCIM. Memerlukan permission users:manage dan fitur enterprise SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18139,7 +18204,8 @@ paths: tags: - Enterprise summary: Cabut token SCIM - description: Cabut bearer token SCIM saat ini. Memerlukan permission users:manage dan fitur enterprise SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18346,7 +18412,8 @@ paths: tags: - Enterprise summary: Impor konfigurasi - description: Impor konfigurasi instance, role kustom, dan teams. Memerlukan permission system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18590,7 +18657,8 @@ paths: tags: - Enterprise summary: Musnahkan data pengguna - description: Musnahkan data pengguna secara permanen setelah konfirmasi eksplisit. Memerlukan permission compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18604,6 +18672,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18612,7 +18682,8 @@ paths: tags: - Enterprise summary: Musnahkan data team - description: Musnahkan data team secara permanen setelah konfirmasi eksplisit. Memerlukan permission compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18626,6 +18697,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19551,34 +19624,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 1d632be8b2f8 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d7797aa68fd4 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: b86ca5298b93 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d7797aa68fd4 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 4e8acd4fab6d - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d7797aa68fd4 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 69b19884d8c7 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d7797aa68fd4 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19743,26 +19800,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 5ea110fb8726 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: be327022d063 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 5b039120a9f3 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 0a7d122242dd paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 448cbf16bcb3 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 55f6263af604 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21343,10 +21388,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: cc7ba13ad8da - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 0874d09c1212 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21471,18 +21512,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 33321fd23a56 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: c8c1a58df6ae paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 86ecb01e3240 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 280ad3c89780 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21607,10 +21640,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 1f25d439e461 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: d67c8db95b97 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21719,18 +21748,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: c7eb7e579da4 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 9c38bf350ea1 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 153f5f2edd22 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 15d45987fda3 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.it.yaml b/apps/api/src/openapi.it.yaml index bcab99ec..be654c67 100644 --- a/apps/api/src/openapi.it.yaml +++ b/apps/api/src/openapi.it.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Crea utente - description: Richiede il permesso users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Aggiorna utente - description: Richiede il permesso users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Elimina utente - description: Richiede il permesso users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Reimposta password utente - description: Richiede il permesso users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Crea un ruolo personalizzato description: | - Crea un nuovo ruolo personalizzato con i permessi specificati. I nomi dei ruoli devono avere - da 2 a 30 caratteri, alfanumerici minuscoli con trattini e trattini bassi. - Richiede il permesso security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Aggiorna un ruolo personalizzato description: | - Aggiorna nome, descrizione o permessi di un ruolo personalizzato. I ruoli - integrati non possono essere modificati. Richiede il permesso security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Elimina un ruolo personalizzato description: | - Elimina un ruolo personalizzato. Gli utenti assegnati a questo ruolo vengono riassegnati al - ruolo "user" predefinito. I ruoli integrati non possono essere eliminati. Richiede il permesso - security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17799,7 +17862,8 @@ paths: tags: - Auth summary: Reimposta MFA utente - description: Reimpostazione da amministratore della registrazione MFA di un utente. Richiede il permesso users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18126,7 +18190,8 @@ paths: tags: - Enterprise summary: Crea token SCIM - description: Genera un token bearer SCIM. Richiede il permesso users:manage e la funzionalità enterprise SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18139,7 +18204,8 @@ paths: tags: - Enterprise summary: Revoca token SCIM - description: Revoca il token bearer SCIM corrente. Richiede il permesso users:manage e la funzionalità enterprise SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18346,7 +18412,8 @@ paths: tags: - Enterprise summary: Importa configurazione - description: Importa la configurazione dell'istanza, i ruoli personalizzati e i team. Richiede il permesso system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18590,7 +18657,8 @@ paths: tags: - Enterprise summary: Elimina dati utente - description: Elimina definitivamente i dati di un utente dopo conferma esplicita. Richiede il permesso compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18604,6 +18672,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18612,7 +18682,8 @@ paths: tags: - Enterprise summary: Elimina dati team - description: Elimina definitivamente i dati di un team dopo conferma esplicita. Richiede il permesso compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18626,6 +18697,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19551,34 +19624,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 39af5ea48610 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 0e5765e979d8 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 147d9642b1ed - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 0e5765e979d8 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 46e93c8cf169 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 0e5765e979d8 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: fad4376b4e3a - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 0e5765e979d8 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19743,26 +19800,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 3f776956d6d7 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 52642299532e paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 4aaff68b2662 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: e9530306309e paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 1a294556e3d6 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: bbdd585f89b8 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21343,10 +21388,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 4d59c8a350cc - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 28eb4e67cee7 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21471,18 +21512,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 4210eadae69c - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: eea79e82ce18 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 98bbae562b16 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 88ba9f6fec4a paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21607,10 +21640,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: a5c7f6cb48a5 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: faf271a0ebb5 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21719,18 +21748,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 5cad02213682 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 6675f5b7370d paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 3fe7cd9295b5 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 825a5859a806 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.ja.yaml b/apps/api/src/openapi.ja.yaml index 4308f5be..bee5704d 100644 --- a/apps/api/src/openapi.ja.yaml +++ b/apps/api/src/openapi.ja.yaml @@ -5783,7 +5783,8 @@ paths: tags: - Auth summary: ユーザーの作成 - description: users:manage 権限が必要です。 + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5854,7 +5855,8 @@ paths: tags: - Auth summary: ユーザーの更新 - description: users:manage 権限が必要です。 + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5909,7 +5911,8 @@ paths: tags: - Auth summary: ユーザーの削除 - description: users:manage 権限が必要です。 + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5947,7 +5950,8 @@ paths: tags: - Auth summary: ユーザーパスワードのリセット - description: users:manage 権限が必要です。 + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7117,7 +7121,10 @@ paths: - Roles summary: カスタムロールの作成 description: | - 指定した権限を持つ新しいカスタムロールを作成します。ロール名は 2〜30 文字で、小文字の英数字にハイフンとアンダースコアを使用できます。security:manage 権限が必要です。 + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7147,7 +7154,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7200,7 +7208,10 @@ paths: - Roles summary: カスタムロールの更新 description: | - カスタムロールの名前、説明、権限を更新します。組み込みロールは変更できません。security:manage 権限が必要です。 + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7274,7 +7285,10 @@ paths: - Roles summary: カスタムロールの削除 description: | - カスタムロールを削除します。このロールが割り当てられているユーザーはデフォルトの「user」ロールに再割り当てされます。組み込みロールは削除できません。security:manage 権限が必要です。 + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12047,6 +12061,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17723,7 +17791,8 @@ paths: tags: - Auth summary: ユーザー MFA のリセット - description: ユーザーの MFA 登録を管理者がリセットします。users:manage 権限が必要です。 + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18050,7 +18119,8 @@ paths: tags: - Enterprise summary: SCIM トークンの作成 - description: SCIM ベアラートークンを生成します。users:manage 権限とエンタープライズ SCIM 機能が必要です。 + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18063,7 +18133,8 @@ paths: tags: - Enterprise summary: SCIM トークンの失効 - description: 現在の SCIM ベアラートークンを失効させます。users:manage 権限とエンタープライズ SCIM 機能が必要です。 + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18270,7 +18341,8 @@ paths: tags: - Enterprise summary: 設定のインポート - description: インスタンス設定、カスタムロール、チームをインポートします。system:health 権限が必要です。 + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18514,7 +18586,8 @@ paths: tags: - Enterprise summary: ユーザーデータの完全削除 - description: 明示的な確認の後、ユーザーのデータを恒久的に完全削除します。compliance:manage 権限が必要です。 + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18528,6 +18601,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18536,7 +18611,8 @@ paths: tags: - Enterprise summary: チームデータの完全削除 - description: 明示的な確認の後、チームのデータを恒久的に完全削除します。compliance:manage 権限が必要です。 + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18550,6 +18626,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19475,34 +19553,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 62b96d8096f0 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: b5a15da87ccd paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: a399c72dbe03 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: b5a15da87ccd paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 560803f5aa97 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: b5a15da87ccd paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 401e65a13a7f - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: b5a15da87ccd paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19667,26 +19729,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 19e49c3dad64 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 3a506079cd32 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 0d228a56532b - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: f48dda7570b3 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 7a00dca6075f - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: a5bc43dcc141 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21267,10 +21317,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: f11fe9de1839 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: f623c780de5a paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21395,18 +21441,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 554df0aa342c - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 5115e5701562 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: f54e7a615d87 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 5a28097ae2e9 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21531,10 +21569,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 9658e7ae2aa5 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 277b03d84d4f paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21643,18 +21677,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 7d3d2d0f7f73 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: b471b9c3b40e paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 7928afcd1a35 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 9b8da1e0afa7 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.ko.yaml b/apps/api/src/openapi.ko.yaml index e8a58b94..8bf3da77 100644 --- a/apps/api/src/openapi.ko.yaml +++ b/apps/api/src/openapi.ko.yaml @@ -5817,7 +5817,8 @@ paths: tags: - Auth summary: 사용자 생성 - description: users:manage 권한이 필요합니다. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5888,7 +5889,8 @@ paths: tags: - Auth summary: 사용자 업데이트 - description: users:manage 권한이 필요합니다. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5943,7 +5945,8 @@ paths: tags: - Auth summary: 사용자 삭제 - description: users:manage 권한이 필요합니다. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5981,7 +5984,8 @@ paths: tags: - Auth summary: 사용자 비밀번호 재설정 - description: users:manage 권한이 필요합니다. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7158,9 +7162,10 @@ paths: - Roles summary: 사용자 정의 역할 생성 description: | - 지정된 권한으로 새 사용자 정의 역할을 생성합니다. 역할 이름은 2~30자, - 소문자 영숫자에 하이픈과 밑줄을 사용할 수 있습니다. security:manage - 권한이 필요합니다. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7190,7 +7195,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7243,8 +7249,10 @@ paths: - Roles summary: 사용자 정의 역할 업데이트 description: | - 사용자 정의 역할의 이름, 설명 또는 권한을 업데이트합니다. 기본 제공 - 역할은 수정할 수 없습니다. security:manage 권한이 필요합니다. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7318,9 +7326,10 @@ paths: - Roles summary: 사용자 정의 역할 삭제 description: | - 사용자 정의 역할을 삭제합니다. 이 역할에 할당된 사용자는 기본 "user" - 역할로 재할당됩니다. 기본 제공 역할은 삭제할 수 없습니다. security:manage - 권한이 필요합니다. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12110,6 +12119,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17797,7 +17860,8 @@ paths: tags: - Auth summary: 사용자 MFA 재설정 - description: 사용자의 MFA 등록을 관리자가 재설정합니다. users:manage 권한이 필요합니다. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18124,7 +18188,8 @@ paths: tags: - Enterprise summary: SCIM 토큰 생성 - description: SCIM bearer 토큰을 생성합니다. users:manage 권한과 엔터프라이즈 SCIM 기능이 필요합니다. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18137,7 +18202,8 @@ paths: tags: - Enterprise summary: SCIM 토큰 취소 - description: 현재 SCIM bearer 토큰을 취소합니다. users:manage 권한과 엔터프라이즈 SCIM 기능이 필요합니다. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18344,7 +18410,8 @@ paths: tags: - Enterprise summary: 구성 가져오기 - description: 인스턴스 구성, 사용자 정의 역할, 팀을 가져옵니다. system:health 권한이 필요합니다. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18588,7 +18655,8 @@ paths: tags: - Enterprise summary: 사용자 데이터 완전 삭제 - description: 명시적 확인 후 사용자의 데이터를 영구적으로 완전 삭제합니다. compliance:manage 권한이 필요합니다. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18602,6 +18670,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18610,7 +18680,8 @@ paths: tags: - Enterprise summary: 팀 데이터 완전 삭제 - description: 명시적 확인 후 팀의 데이터를 영구적으로 완전 삭제합니다. compliance:manage 권한이 필요합니다. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18624,6 +18695,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19549,34 +19622,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: a973ecff9628 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1c52fe88f9fd paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 0ca7eeb14150 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1c52fe88f9fd paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: daed5e302501 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1c52fe88f9fd paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: d8f3a8cc1f20 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 1c52fe88f9fd paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19741,26 +19798,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: d3683815f9ca - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 38f256e267e7 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: ae22ec11c5be - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: e96d8982c21c paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 8bf1ec599db1 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 9e9159f50086 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21341,10 +21386,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: c1ff6c27c3b9 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 82f539a938ba paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21469,18 +21510,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 8f7167b43c5c - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 2dee1a70e030 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 53db624a3ebf - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 63ac3602652c paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21605,10 +21638,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 545bc5835bce - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 5681893d00ce paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21717,18 +21746,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 8871183cdf30 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: cfbca8c571da paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: eca4768c129e - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 5f750104bc06 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.nl.yaml b/apps/api/src/openapi.nl.yaml index 570f96c5..b11bfe3c 100644 --- a/apps/api/src/openapi.nl.yaml +++ b/apps/api/src/openapi.nl.yaml @@ -5783,7 +5783,8 @@ paths: tags: - Auth summary: Gebruiker aanmaken - description: Vereist de permissie users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5854,7 +5855,8 @@ paths: tags: - Auth summary: Gebruiker bijwerken - description: Vereist de permissie users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5909,7 +5911,8 @@ paths: tags: - Auth summary: Gebruiker verwijderen - description: Vereist de permissie users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5947,7 +5950,8 @@ paths: tags: - Auth summary: Gebruikerswachtwoord opnieuw instellen - description: Vereist de permissie users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7116,7 +7120,10 @@ paths: - Roles summary: Een aangepaste rol aanmaken description: | - Maak een nieuwe aangepaste rol aan met opgegeven permissies. Rolnamen moeten 2-30 tekens lang zijn, kleine letters, cijfers, met koppeltekens en underscores. Vereist de permissie security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7146,7 +7153,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7199,7 +7207,10 @@ paths: - Roles summary: Een aangepaste rol bijwerken description: | - Werk naam, beschrijving of permissies van een aangepaste rol bij. Ingebouwde rollen kunnen niet worden gewijzigd. Vereist de permissie security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7273,7 +7284,10 @@ paths: - Roles summary: Een aangepaste rol verwijderen description: | - Verwijder een aangepaste rol. Gebruikers met deze rol worden opnieuw toegewezen aan de standaardrol "user". Ingebouwde rollen kunnen niet worden verwijderd. Vereist de permissie security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12046,6 +12060,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17722,7 +17790,8 @@ paths: tags: - Auth summary: Gebruikers-MFA opnieuw instellen - description: Admin-reset van de MFA-inschrijving van een gebruiker. Vereist de permissie users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18049,7 +18118,8 @@ paths: tags: - Enterprise summary: SCIM-token aanmaken - description: Genereer een SCIM-bearertoken. Vereist de permissie users:manage en de enterprise SCIM-feature. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18062,7 +18132,8 @@ paths: tags: - Enterprise summary: SCIM-token herroepen - description: Herroep het huidige SCIM-bearertoken. Vereist de permissie users:manage en de enterprise SCIM-feature. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18269,7 +18340,8 @@ paths: tags: - Enterprise summary: Configuratie importeren - description: Importeer instanceconfiguratie, aangepaste rollen en teams. Vereist de permissie system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18513,7 +18585,8 @@ paths: tags: - Enterprise summary: Gebruikersdata wissen - description: Wis de data van een gebruiker permanent na expliciete bevestiging. Vereist de permissie compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18527,6 +18600,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18535,7 +18610,8 @@ paths: tags: - Enterprise summary: Teamdata wissen - description: Wis de data van een team permanent na expliciete bevestiging. Vereist de permissie compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18549,6 +18625,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19474,34 +19552,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: ce03c809be1b - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: eee4a7420b1a paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: a393da24ea5d - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: eee4a7420b1a paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: cf98af9c31fd - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: eee4a7420b1a paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: d64fc7e8f826 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: eee4a7420b1a paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19666,26 +19728,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: a5a416c7564d - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 79049baeccb4 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: d30e6617857a - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 66cefbdb630d paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 75959ba4575d - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: ff47c0829774 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21266,10 +21316,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 1a55beacb16e - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: bceeeb58d820 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21394,18 +21440,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: e54726ac9ea0 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 3ac6cb2cd5f5 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 2f66dc101e3c - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 204b696c65eb paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21530,10 +21568,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: e879e533fcf9 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: b2a71ae40d36 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21642,18 +21676,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 95d4da8d810b - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 3cffb6dfe3a8 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 45f1f9377231 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 37cfb511524d paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.pl.yaml b/apps/api/src/openapi.pl.yaml index e7653143..fed79207 100644 --- a/apps/api/src/openapi.pl.yaml +++ b/apps/api/src/openapi.pl.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Utwórz użytkownika - description: Wymaga uprawnienia users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Aktualizuj użytkownika - description: Wymaga uprawnienia users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Usuń użytkownika - description: Wymaga uprawnienia users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Zresetuj hasło użytkownika - description: Wymaga uprawnienia users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Utwórz niestandardową rolę description: | - Utwórz nową niestandardową rolę z określonymi uprawnieniami. Nazwy ról muszą mieć - 2-30 znaków, małe litery alfanumeryczne z myślnikami i podkreśleniami. - Wymaga uprawnienia security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Aktualizuj niestandardową rolę description: | - Aktualizuj nazwę, opis lub uprawnienia niestandardowej roli. Ról wbudowanych - nie można modyfikować. Wymaga uprawnienia security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Usuń niestandardową rolę description: | - Usuń niestandardową rolę. Użytkownicy przypisani do tej roli są ponownie przypisywani do - domyślnej roli "user". Ról wbudowanych nie można usunąć. Wymaga uprawnienia - security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17799,7 +17862,8 @@ paths: tags: - Auth summary: Zresetuj MFA użytkownika - description: Administracyjny reset rejestracji MFA użytkownika. Wymaga uprawnienia users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18126,7 +18190,8 @@ paths: tags: - Enterprise summary: Utwórz token SCIM - description: Wygeneruj token bearer SCIM. Wymaga uprawnienia users:manage i funkcji enterprise SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18139,7 +18204,8 @@ paths: tags: - Enterprise summary: Odwołaj token SCIM - description: Odwołaj bieżący token bearer SCIM. Wymaga uprawnienia users:manage i funkcji enterprise SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18346,7 +18412,8 @@ paths: tags: - Enterprise summary: Importuj konfigurację - description: Importuj konfigurację instancji, niestandardowe role i zespoły. Wymaga uprawnienia system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18590,7 +18657,8 @@ paths: tags: - Enterprise summary: Wyczyść dane użytkownika - description: Trwale wyczyść dane użytkownika po jawnym potwierdzeniu. Wymaga uprawnienia compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18604,6 +18672,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18612,7 +18682,8 @@ paths: tags: - Enterprise summary: Wyczyść dane zespołu - description: Trwale wyczyść dane zespołu po jawnym potwierdzeniu. Wymaga uprawnienia compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18626,6 +18697,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19551,34 +19624,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 2f5735ea4d1b - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 860e953e5946 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 494814d62c6a - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 860e953e5946 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 16cab5ec4ea0 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 860e953e5946 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 6ce9399937b3 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 860e953e5946 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19743,26 +19800,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 65b34c923d31 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 13000c0366bd paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 8026d216de48 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 08014427e3ae paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 38e5acf2183f - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 73866934c720 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21343,10 +21388,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: '996109115791' - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 3219e392822e paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21471,18 +21512,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 258d73dca596 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: d54fe12aea53 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 54f8541abd2c - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 02eeded6b245 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21607,10 +21640,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 69d9e498d678 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 124ca984c908 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21719,18 +21748,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: a94e09cbe3db - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: e56f6948bbd9 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: b0621284beab - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: b869f0bc7a3e paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.pt-BR.yaml b/apps/api/src/openapi.pt-BR.yaml index 05eaf2f4..1b7a4e53 100644 --- a/apps/api/src/openapi.pt-BR.yaml +++ b/apps/api/src/openapi.pt-BR.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Criar usuário - description: Requer a permissão users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Atualizar usuário - description: Requer a permissão users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Excluir usuário - description: Requer a permissão users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Redefinir senha do usuário - description: Requer a permissão users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Criar uma função personalizada description: | - Cria uma nova função personalizada com as permissões especificadas. Os nomes de função devem ter - de 2 a 30 caracteres, alfanuméricos minúsculos com hifens e sublinhados. - Requer a permissão security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Atualizar uma função personalizada description: | - Atualiza nome, descrição ou permissões de uma função personalizada. Funções - integradas não podem ser modificadas. Requer a permissão security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Excluir uma função personalizada description: | - Exclui uma função personalizada. Usuários atribuídos a esta função são reatribuídos à - função "user" padrão. Funções integradas não podem ser excluídas. Requer - a permissão security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17799,7 +17862,8 @@ paths: tags: - Auth summary: Redefinir MFA do usuário - description: Redefinição administrativa do cadastro de MFA de um usuário. Requer a permissão users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18126,7 +18190,8 @@ paths: tags: - Enterprise summary: Criar token SCIM - description: Gera um bearer token SCIM. Requer a permissão users:manage e o recurso enterprise SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18139,7 +18204,8 @@ paths: tags: - Enterprise summary: Revogar token SCIM - description: Revoga o bearer token SCIM atual. Requer a permissão users:manage e o recurso enterprise SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18346,7 +18412,8 @@ paths: tags: - Enterprise summary: Importar configuração - description: Importa a configuração da instância, funções personalizadas e equipes. Requer a permissão system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18590,7 +18657,8 @@ paths: tags: - Enterprise summary: Expurgar dados do usuário - description: Expurga permanentemente os dados de um usuário após confirmação explícita. Requer a permissão compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18604,6 +18672,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18612,7 +18682,8 @@ paths: tags: - Enterprise summary: Expurgar dados da equipe - description: Expurga permanentemente os dados de uma equipe após confirmação explícita. Requer a permissão compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18626,6 +18697,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19551,34 +19624,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: '127384371311' - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: fdb8fb70948e paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 7738a0af8d16 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: fdb8fb70948e paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 6d396d03df00 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: fdb8fb70948e paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: a59c25c6b84d - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: fdb8fb70948e paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19743,26 +19800,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 1defc74bb77e - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: d38e119a3546 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 67042dd5db3c - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 8c43fbf09e1b paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 835fbe82053f - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: bf16081c8889 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21343,10 +21388,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 6a278a5509e3 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 7246b0ed52d3 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21471,18 +21512,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 92db7f92e032 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 03cc8cdab4d2 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: c7831758ca93 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 5a142d3a757e paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21607,10 +21640,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 79e7be0dfa9f - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: fc6e1d215512 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21719,18 +21748,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 9411fe4291a6 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 9e7e7ff40f52 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 89d3c380c9a3 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 247895de335f paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.ru.yaml b/apps/api/src/openapi.ru.yaml index e2752fea..12378a51 100644 --- a/apps/api/src/openapi.ru.yaml +++ b/apps/api/src/openapi.ru.yaml @@ -5820,7 +5820,8 @@ paths: tags: - Auth summary: Создание пользователя - description: Требует разрешение users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5891,7 +5892,8 @@ paths: tags: - Auth summary: Обновление пользователя - description: Требует разрешение users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5946,7 +5948,8 @@ paths: tags: - Auth summary: Удаление пользователя - description: Требует разрешение users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5984,7 +5987,8 @@ paths: tags: - Auth summary: Сброс пароля пользователя - description: Требует разрешение users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7162,9 +7166,10 @@ paths: - Roles summary: Создание пользовательской роли description: | - Создание новой пользовательской роли с указанными разрешениями. Имена ролей - должны быть длиной 2-30 символов, строчные буквенно-цифровые с дефисами и - подчёркиваниями. Требует разрешение security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7194,7 +7199,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7247,8 +7253,10 @@ paths: - Roles summary: Обновление пользовательской роли description: | - Обновление имени, описания или разрешений пользовательской роли. Встроенные - роли изменить нельзя. Требует разрешение security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7322,9 +7330,10 @@ paths: - Roles summary: Удаление пользовательской роли description: | - Удаление пользовательской роли. Пользователи, назначенные на эту роль, - переназначаются на роль "user" по умолчанию. Встроенные роли удалить нельзя. - Требует разрешение security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12114,6 +12123,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17801,7 +17864,8 @@ paths: tags: - Auth summary: Сброс MFA пользователя - description: Административный сброс регистрации MFA пользователя. Требует разрешение users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18128,7 +18192,8 @@ paths: tags: - Enterprise summary: Создание токена SCIM - description: Создание bearer-токена SCIM. Требует разрешение users:manage и enterprise-функцию SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18141,7 +18206,8 @@ paths: tags: - Enterprise summary: Отзыв токена SCIM - description: Отзыв текущего bearer-токена SCIM. Требует разрешение users:manage и enterprise-функцию SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18348,7 +18414,8 @@ paths: tags: - Enterprise summary: Импорт конфигурации - description: Импорт конфигурации экземпляра, пользовательских ролей и команд. Требует разрешение system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18592,7 +18659,8 @@ paths: tags: - Enterprise summary: Очистка данных пользователя - description: Безвозвратная очистка данных пользователя после явного подтверждения. Требует разрешение compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18606,6 +18674,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18614,7 +18684,8 @@ paths: tags: - Enterprise summary: Очистка данных команды - description: Безвозвратная очистка данных команды после явного подтверждения. Требует разрешение compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18628,6 +18699,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19553,34 +19626,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 5e4451c5086e - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: bf7e502ea3e7 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: aa37c465c96c - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: bf7e502ea3e7 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 1206949b85c0 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: bf7e502ea3e7 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 2473e7b1ab71 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: bf7e502ea3e7 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19745,26 +19802,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 10b7cc698973 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 45a580f516a0 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 298d4b187113 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 9fd495d86e41 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: d6beacf82161 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: e549ddce5f92 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21345,10 +21390,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 1df0d3057e14 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 2952bf9200c7 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21473,18 +21514,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: ab246e48961a - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 78598ae2923d paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 7a0d16479d60 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 0ca5dad68219 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21609,10 +21642,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: ad05d605d6dc - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: b2ecb106335e paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21721,18 +21750,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 4f29e3423dc5 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: d09ba922ac43 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: f0da408b859a - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: cfbe69db48a7 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.sv.yaml b/apps/api/src/openapi.sv.yaml index 3a3ad551..ab8b0eee 100644 --- a/apps/api/src/openapi.sv.yaml +++ b/apps/api/src/openapi.sv.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Skapa anvandare - description: Kraver behorigheten users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Uppdatera anvandare - description: Kraver behorigheten users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Ta bort anvandare - description: Kraver behorigheten users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Aaterstall anvandarlosenord - description: Kraver behorigheten users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Skapa en anpassad roll description: | - Skapa en ny anpassad roll med angivna behorigheter. Rollnamn maaste vara - 2-30 tecken, gemener alfanumeriska med bindestreck och understreck. - Kraver behorigheten security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Uppdatera en anpassad roll description: | - Uppdatera namn, beskrivning eller behorigheter for en anpassad roll. Inbyggda - roller kan inte andras. Kraver behorigheten security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Ta bort en anpassad roll description: | - Ta bort en anpassad roll. Anvandare som tilldelats denna roll flyttas till - standardrollen "user". Inbyggda roller kan inte tas bort. Kraver - behorigheten security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17798,7 +17861,8 @@ paths: tags: - Auth summary: Aaterstall anvandarens MFA - description: Admin-aaterstallning av en anvandares MFA-registrering. Kraver behorigheten users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18125,7 +18189,8 @@ paths: tags: - Enterprise summary: Skapa SCIM-token - description: Generera en SCIM-bearer-token. Kraver behorigheten users:manage och enterprise SCIM-funktionen. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18138,7 +18203,8 @@ paths: tags: - Enterprise summary: Aaterkalla SCIM-token - description: Aaterkalla den aktuella SCIM-bearer-token. Kraver behorigheten users:manage och enterprise SCIM-funktionen. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18345,7 +18411,8 @@ paths: tags: - Enterprise summary: Importera konfiguration - description: Importera instanskonfiguration, anpassade roller och team. Kraver behorigheten system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18589,7 +18656,8 @@ paths: tags: - Enterprise summary: Rensa anvandardata - description: Rensa permanent en anvandares data efter explicit bekraftelse. Kraver behorigheten compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18603,6 +18671,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18611,7 +18681,8 @@ paths: tags: - Enterprise summary: Rensa teamdata - description: Rensa permanent ett teams data efter explicit bekraftelse. Kraver behorigheten compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18625,6 +18696,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19550,34 +19623,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: d530ca2f130a - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: c43572fecf5e paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: c17ff2c38d74 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: c43572fecf5e paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: e81883edd0d7 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: c43572fecf5e paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: f675b6a8559a - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: c43572fecf5e paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19742,26 +19799,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 3675433b4823 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: ade0411b315b paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 2e2603cebd47 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 5545418b81df paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 014c9d1a1a61 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: fce12718019e paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21342,10 +21387,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 43c58a9bf9df - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 4aa75947b7fb paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21470,18 +21511,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: d3b506ae8c46 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 77986a97327f paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: bc550bee10c1 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 53cc26393110 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21606,10 +21639,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 44be217903e1 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: dcc3efe6bac7 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21718,18 +21747,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: c0c35851fd99 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 3ab04ee58dd3 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 00f4e1a87644 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: b6874c3c1961 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.th.yaml b/apps/api/src/openapi.th.yaml index 674bdd5d..a226aea9 100644 --- a/apps/api/src/openapi.th.yaml +++ b/apps/api/src/openapi.th.yaml @@ -5782,7 +5782,8 @@ paths: tags: - Auth summary: สร้างผู้ใช้ - description: ต้องมีสิทธิ์ users:manage + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5853,7 +5854,8 @@ paths: tags: - Auth summary: อัปเดตผู้ใช้ - description: ต้องมีสิทธิ์ users:manage + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5908,7 +5910,8 @@ paths: tags: - Auth summary: ลบผู้ใช้ - description: ต้องมีสิทธิ์ users:manage + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5946,7 +5949,8 @@ paths: tags: - Auth summary: รีเซ็ตรหัสผ่านผู้ใช้ - description: ต้องมีสิทธิ์ users:manage + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7115,7 +7119,10 @@ paths: - Roles summary: สร้างบทบาทแบบกำหนดเอง description: | - สร้างบทบาทกำหนดเองใหม่ด้วยสิทธิ์ที่ระบุ ชื่อบทบาทต้องมี 2-30 อักขระ เป็นตัวพิมพ์เล็กผสมตัวเลข พร้อมขีดกลางและขีดล่าง ต้องมีสิทธิ์ security:manage + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7145,7 +7152,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7198,7 +7206,10 @@ paths: - Roles summary: อัปเดตบทบาทแบบกำหนดเอง description: | - อัปเดตชื่อ คำอธิบาย หรือสิทธิ์ของบทบาทกำหนดเอง บทบาทในตัวแก้ไขไม่ได้ ต้องมีสิทธิ์ security:manage + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7272,7 +7283,10 @@ paths: - Roles summary: ลบบทบาทแบบกำหนดเอง description: | - ลบบทบาทกำหนดเอง ผู้ใช้ที่ถูกกำหนดให้บทบาทนี้จะถูกย้ายไปยังบทบาท "user" เริ่มต้น บทบาทในตัวลบไม่ได้ ต้องมีสิทธิ์ security:manage + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12045,6 +12059,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17721,7 +17789,8 @@ paths: tags: - Auth summary: รีเซ็ต MFA ของผู้ใช้ - description: แอดมินรีเซ็ตการลงทะเบียน MFA ของผู้ใช้ ต้องมีสิทธิ์ users:manage + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18048,7 +18117,8 @@ paths: tags: - Enterprise summary: สร้างโทเคน SCIM - description: สร้าง SCIM bearer token ต้องมีสิทธิ์ users:manage และฟีเจอร์ SCIM ระดับองค์กร + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18061,7 +18131,8 @@ paths: tags: - Enterprise summary: เพิกถอนโทเคน SCIM - description: เพิกถอน SCIM bearer token ปัจจุบัน ต้องมีสิทธิ์ users:manage และฟีเจอร์ SCIM ระดับองค์กร + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18268,7 +18339,8 @@ paths: tags: - Enterprise summary: นำเข้าการกำหนดค่า - description: นำเข้าการกำหนดค่าอินสแตนซ์ บทบาทกำหนดเอง และทีม ต้องมีสิทธิ์ system:health + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18512,7 +18584,8 @@ paths: tags: - Enterprise summary: ล้างข้อมูลผู้ใช้ - description: ล้างข้อมูลของผู้ใช้อย่างถาวรหลังยืนยันอย่างชัดเจน ต้องมีสิทธิ์ compliance:manage + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18526,6 +18599,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18534,7 +18609,8 @@ paths: tags: - Enterprise summary: ล้างข้อมูลทีม - description: ล้างข้อมูลของทีมอย่างถาวรหลังยืนยันอย่างชัดเจน ต้องมีสิทธิ์ compliance:manage + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18548,6 +18624,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19473,34 +19551,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: c5587b1ddffd - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13f40e519ab5 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 0ca7b8464679 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13f40e519ab5 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 1ff1cffdcc26 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13f40e519ab5 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 583aab768075 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13f40e519ab5 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19665,26 +19727,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: d9eaffea5160 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 3e4d09ca46ef paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: a58e84147d89 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: c1a2572fe0f8 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 0c1634a623cd - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: fa9cb32ae50a paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21265,10 +21315,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 05914222a0bf - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 2f8239d24cd7 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21393,18 +21439,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 2c6e7da6ef14 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 30ab53fea8fc paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 7ada5d3fc0d0 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: d6b63119c696 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21529,10 +21567,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: b6a1491a4fde - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 57ef9570b106 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21641,18 +21675,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 7e96422fd68a - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: becdc6dffa66 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: bcd3ea816823 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 48d73045a53f paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.tr.yaml b/apps/api/src/openapi.tr.yaml index 1dff56d5..a5a827df 100644 --- a/apps/api/src/openapi.tr.yaml +++ b/apps/api/src/openapi.tr.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Kullanıcı oluştur - description: users:manage iznini gerektirir. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Kullanıcıyı güncelle - description: users:manage iznini gerektirir. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Kullanıcıyı sil - description: users:manage iznini gerektirir. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Kullanıcı parolasını sıfırla - description: users:manage iznini gerektirir. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Özel bir rol oluştur description: | - Belirtilen izinlerle yeni bir özel rol oluşturun. Rol adları 2-30 - karakter, tire ve alt çizgi içeren küçük harfli alfanümerik olmalıdır. - security:manage iznini gerektirir. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Özel bir rolü güncelle description: | - Özel bir rolün adını, açıklamasını veya izinlerini güncelleyin. Yerleşik - roller değiştirilemez. security:manage iznini gerektirir. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Özel bir rolü sil description: | - Özel bir rolü silin. Bu role atanmış kullanıcılar varsayılan "user" - rolüne yeniden atanır. Yerleşik roller silinemez. security:manage - iznini gerektirir. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17798,7 +17861,8 @@ paths: tags: - Auth summary: Kullanıcı MFA'sını sıfırla - description: Bir kullanıcının MFA kaydının yönetici tarafından sıfırlanması. users:manage iznini gerektirir. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18125,7 +18189,8 @@ paths: tags: - Enterprise summary: SCIM jetonu oluştur - description: Bir SCIM bearer token oluşturun. users:manage iznini ve kurumsal SCIM özelliğini gerektirir. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18138,7 +18203,8 @@ paths: tags: - Enterprise summary: SCIM jetonunu iptal et - description: Mevcut SCIM bearer token'ını iptal edin. users:manage iznini ve kurumsal SCIM özelliğini gerektirir. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18345,7 +18411,8 @@ paths: tags: - Enterprise summary: Yapılandırmayı içe aktar - description: Örnek yapılandırmasını, özel rolleri ve ekipleri içe aktarın. system:health iznini gerektirir. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18589,7 +18656,8 @@ paths: tags: - Enterprise summary: Kullanıcı verilerini temizle - description: Açık onaydan sonra bir kullanıcının verilerini kalıcı olarak temizleyin. compliance:manage iznini gerektirir. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18603,6 +18671,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18611,7 +18681,8 @@ paths: tags: - Enterprise summary: Ekip verilerini temizle - description: Açık onaydan sonra bir ekibin verilerini kalıcı olarak temizleyin. compliance:manage iznini gerektirir. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18625,6 +18696,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19550,34 +19623,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 925ba3158557 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13fdd572a0c3 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: c10b65213ace - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13fdd572a0c3 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 856441e91698 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13fdd572a0c3 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 68581abd9c33 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 13fdd572a0c3 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19742,26 +19799,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: c78d3f7d1e14 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 148b43181c63 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 5869be12f8e7 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 41be553b58aa paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 3a7b318e5360 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 9d1506fca33e paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21342,10 +21387,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: f17b8527e5b9 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 74bfaf2afb77 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21470,18 +21511,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 910fcf9b5a60 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 33d190e3ae03 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: aaf820ad644f - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 90ba248aeb6f paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21606,10 +21639,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 3c49ebff8d5c - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 174b97dec77f paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21718,18 +21747,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: c9ee48e2e87e - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: fad3572206a3 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 30cfe597b55c - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 11a40ea4ce0e paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.uk.yaml b/apps/api/src/openapi.uk.yaml index d30e5be9..6946d1e6 100644 --- a/apps/api/src/openapi.uk.yaml +++ b/apps/api/src/openapi.uk.yaml @@ -5818,7 +5818,8 @@ paths: tags: - Auth summary: Створити користувача - description: Потребує дозволу users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5889,7 +5890,8 @@ paths: tags: - Auth summary: Оновити користувача - description: Потребує дозволу users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5944,7 +5946,8 @@ paths: tags: - Auth summary: Видалити користувача - description: Потребує дозволу users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5982,7 +5985,8 @@ paths: tags: - Auth summary: Скинути пароль користувача - description: Потребує дозволу users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7160,9 +7164,10 @@ paths: - Roles summary: Створити власну роль description: | - Створити нову власну роль із зазначеними дозволами. Назви ролей мають бути - довжиною 2-30 символів, малими літерами й цифрами з дефісами та підкресленнями. - Потребує дозволу security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7192,7 +7197,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7245,8 +7251,10 @@ paths: - Roles summary: Оновити власну роль description: | - Оновити назву, опис або дозволи власної ролі. Вбудовані - ролі змінити не можна. Потребує дозволу security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7320,9 +7328,10 @@ paths: - Roles summary: Видалити власну роль description: | - Видалити власну роль. Користувачів, призначених цій ролі, буде перепризначено на - стандартну роль "user". Вбудовані ролі видалити не можна. Потребує - дозволу security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12112,6 +12121,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17799,7 +17862,8 @@ paths: tags: - Auth summary: Скинути MFA користувача - description: Адміністративне скидання реєстрації MFA користувача. Потребує дозволу users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18126,7 +18190,8 @@ paths: tags: - Enterprise summary: Створити токен SCIM - description: Згенерувати bearer-токен SCIM. Потребує дозволу users:manage та enterprise-функції SCIM. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18139,7 +18204,8 @@ paths: tags: - Enterprise summary: Відкликати токен SCIM - description: Відкликати поточний bearer-токен SCIM. Потребує дозволу users:manage та enterprise-функції SCIM. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18346,7 +18412,8 @@ paths: tags: - Enterprise summary: Імпортувати конфігурацію - description: Імпортувати конфігурацію екземпляра, власні ролі та команди. Потребує дозволу system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18590,7 +18657,8 @@ paths: tags: - Enterprise summary: Очистити дані користувача - description: Остаточно очистити дані користувача після явного підтвердження. Потребує дозволу compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18604,6 +18672,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18612,7 +18682,8 @@ paths: tags: - Enterprise summary: Очистити дані команди - description: Остаточно очистити дані команди після явного підтвердження. Потребує дозволу compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18626,6 +18697,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19551,34 +19624,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 15c947f247b4 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ce657feb23af paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 052a37f3f7b8 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ce657feb23af paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 591461167c11 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ce657feb23af paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 0f5550c0a24d - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: ce657feb23af paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19743,26 +19800,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 45fb1a9e83a6 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: b13d3ee6bf82 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: f59d5d29c51c - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 8d6d47ba1020 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 17d9514bafaa - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 88a044c3c2ad paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21343,10 +21388,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 9361a8782a36 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 2a23f0e6aae7 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21471,18 +21512,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 9ca8595144b6 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: d3e0bb01d9e6 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 17e1baf1c0db - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 8fbe3e84df48 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21607,10 +21640,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 9913cd1c8f54 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: ff109524f670 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21719,18 +21748,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 8323ebcab76b - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 1c10ef62c49d paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: d402d13a6530 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: df502326bd95 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.vi.yaml b/apps/api/src/openapi.vi.yaml index 5fe9c5a8..f5afcc49 100644 --- a/apps/api/src/openapi.vi.yaml +++ b/apps/api/src/openapi.vi.yaml @@ -5817,7 +5817,8 @@ paths: tags: - Auth summary: Tạo người dùng - description: Yêu cầu quyền users:manage. + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5888,7 +5889,8 @@ paths: tags: - Auth summary: Cập nhật người dùng - description: Yêu cầu quyền users:manage. + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5943,7 +5945,8 @@ paths: tags: - Auth summary: Xóa người dùng - description: Yêu cầu quyền users:manage. + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5981,7 +5984,8 @@ paths: tags: - Auth summary: Đặt lại mật khẩu người dùng - description: Yêu cầu quyền users:manage. + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7159,9 +7163,10 @@ paths: - Roles summary: Tạo một vai trò tùy chỉnh description: | - Tạo một vai trò tùy chỉnh mới với các quyền được chỉ định. Tên vai trò phải - từ 2-30 ký tự, chữ thường chữ và số với dấu gạch nối và dấu gạch dưới. - Yêu cầu quyền security:manage. + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7191,7 +7196,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7244,8 +7250,10 @@ paths: - Roles summary: Cập nhật một vai trò tùy chỉnh description: | - Cập nhật tên, mô tả hoặc quyền của một vai trò tùy chỉnh. Các vai trò tích hợp - sẵn không thể sửa đổi. Yêu cầu quyền security:manage. + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7319,9 +7327,10 @@ paths: - Roles summary: Xóa một vai trò tùy chỉnh description: | - Xóa một vai trò tùy chỉnh. Người dùng được gán vai trò này sẽ được gán lại - vai trò "user" mặc định. Các vai trò tích hợp sẵn không thể xóa. Yêu cầu - quyền security:manage. + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12111,6 +12120,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17798,7 +17861,8 @@ paths: tags: - Auth summary: Đặt lại MFA người dùng - description: Admin đặt lại đăng ký MFA của một người dùng. Yêu cầu quyền users:manage. + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18124,7 +18188,8 @@ paths: tags: - Enterprise summary: Tạo token SCIM - description: Tạo một bearer token SCIM. Yêu cầu quyền users:manage và tính năng SCIM enterprise. + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18137,7 +18202,8 @@ paths: tags: - Enterprise summary: Thu hồi token SCIM - description: Thu hồi bearer token SCIM hiện tại. Yêu cầu quyền users:manage và tính năng SCIM enterprise. + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18344,7 +18410,8 @@ paths: tags: - Enterprise summary: Nhập cấu hình - description: Nhập cấu hình phiên bản, vai trò tùy chỉnh và nhóm. Yêu cầu quyền system:health. + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18588,7 +18655,8 @@ paths: tags: - Enterprise summary: Xóa sạch dữ liệu người dùng - description: Xóa sạch vĩnh viễn dữ liệu của một người dùng sau khi xác nhận rõ ràng. Yêu cầu quyền compliance:manage. + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18602,6 +18670,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18610,7 +18680,8 @@ paths: tags: - Enterprise summary: Xóa sạch dữ liệu nhóm - description: Xóa sạch vĩnh viễn dữ liệu của một nhóm sau khi xác nhận rõ ràng. Yêu cầu quyền compliance:manage. + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18624,6 +18695,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19549,34 +19622,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 70f60575a167 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 6c0239884430 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: d74ccea40e08 - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 6c0239884430 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: e5cbc0c1e0ce - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 6c0239884430 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 7faec0047b9c - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 6c0239884430 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19741,26 +19798,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: 43349a43163f - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: 50d7b491b6ee paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: ccab346f4d92 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 2a9ead7b4450 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: 3388ceef4e88 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 10dc6469f6fb paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21341,10 +21386,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: 976680e3482c - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 9565ece5066f paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21469,18 +21510,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: b100b24327ad - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: 630d9ba98fa0 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: e7b3df260dba - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 6d62d4ad0173 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21605,10 +21638,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: 626289366f67 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 51bdac4ae503 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21717,18 +21746,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 94b0325b943f - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 5b75dcee6135 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: 9ec8343334f2 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: d9506dc5bbf1 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.yaml b/apps/api/src/openapi.yaml index d974443d..aad4ab9f 100644 --- a/apps/api/src/openapi.yaml +++ b/apps/api/src/openapi.yaml @@ -5695,7 +5695,10 @@ paths: operationId: createUser tags: [Auth] summary: Create user - description: Requires users:manage permission. + description: > + Requires users:manage permission. The assigned role must be within the + actor's effective role authority, including hierarchy, permissions, + tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5764,7 +5767,10 @@ paths: operationId: updateUser tags: [Auth] summary: Update user - description: Requires users:manage permission. + description: > + Requires users:manage permission and authority over the target's + current role. Any newly assigned role must also be within the actor's + effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5818,7 +5824,10 @@ paths: operationId: deleteUser tags: [Auth] summary: Delete user - description: Requires users:manage permission. + description: > + Requires users:manage permission and authority over the target's + current role, including hierarchy, permissions, tool access, and any + API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5856,7 +5865,10 @@ paths: operationId: resetUserPassword tags: [Auth] summary: Reset user password - description: Requires users:manage permission. + description: > + Reset a local user's password. Requires users:manage permission and + authority over the target's current role, including hierarchy, + permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -6983,7 +6995,8 @@ paths: description: | Create a new custom role with specified permissions. Role names must be 2-30 characters, lowercase alphanumeric with hyphens and underscores. - Requires security:manage permission. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7011,7 +7024,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: "201": description: Role created @@ -7065,7 +7079,9 @@ paths: summary: Update a custom role description: | Update name, description, or permissions of a custom role. Built-in - roles cannot be modified. Requires security:manage permission. + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7140,7 +7156,8 @@ paths: description: | Delete a custom role. Users assigned to this role are reassigned to the default "user" role. Built-in roles cannot be deleted. Requires - security:manage permission. + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -17463,7 +17480,10 @@ paths: operationId: resetUserMfa tags: [Auth] summary: Reset user MFA - description: Admin reset of a user's MFA enrollment. Requires users:manage permission. + description: > + Reset a user's MFA enrollment. Requires users:manage permission and + authority over the target's current role, including hierarchy, + permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -17775,7 +17795,11 @@ paths: operationId: createScimToken tags: [Enterprise] summary: Create SCIM token - description: Generate a SCIM bearer token. Requires users:manage permission and the enterprise SCIM feature. + description: > + Generate a global SCIM bearer token. Requires the enterprise SCIM + feature and full effective built-in administrator authority; custom + roles and API keys missing any administrator permission do not qualify. + Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -17787,7 +17811,10 @@ paths: operationId: revokeScimToken tags: [Enterprise] summary: Revoke SCIM token - description: Revoke the current SCIM bearer token. Requires users:manage permission and the enterprise SCIM feature. + description: > + Revoke the global SCIM bearer token. Requires the enterprise SCIM + feature and full effective built-in administrator authority; custom + roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -17988,7 +18015,10 @@ paths: operationId: importEnterpriseConfig tags: [Enterprise] summary: Import configuration - description: Import instance configuration, custom roles, and teams. Requires system:health permission. + description: > + Import instance configuration, custom roles, and teams. Requires full + effective built-in administrator authority; custom roles and API keys + missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18227,7 +18257,11 @@ paths: operationId: purgeEnterpriseUser tags: [Enterprise] summary: Purge user data - description: Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission. + description: > + Permanently purge a user's data after explicit confirmation. Requires + compliance:manage permission and authority over the target's current + role, including hierarchy, permissions, tool access, and any API-key + scope. security: - bearerAuth: [] parameters: @@ -18241,6 +18275,8 @@ paths: description: User purged "400": description: Confirmation required + "403": + description: Feature not licensed, insufficient permission, or target role beyond actor authority "409": description: User or team is under legal hold @@ -18249,7 +18285,11 @@ paths: operationId: purgeEnterpriseTeam tags: [Enterprise] summary: Purge team data - description: Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission. + description: > + Permanently purge a team's data after explicit confirmation. Requires + compliance:manage permission and authority over every member's current + role, including hierarchy, permissions, tool access, and any API-key + scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18263,6 +18303,8 @@ paths: description: Team purged "400": description: Confirmation required + "403": + description: Feature not licensed, insufficient permission, or a member role beyond actor authority "409": description: Team is under legal hold diff --git a/apps/api/src/openapi.zh-CN.yaml b/apps/api/src/openapi.zh-CN.yaml index 044e55c6..1fc6a3f2 100644 --- a/apps/api/src/openapi.zh-CN.yaml +++ b/apps/api/src/openapi.zh-CN.yaml @@ -5782,7 +5782,8 @@ paths: tags: - Auth summary: 创建用户 - description: 需要 users:manage 权限。 + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5853,7 +5854,8 @@ paths: tags: - Auth summary: 更新用户 - description: 需要 users:manage 权限。 + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5908,7 +5910,8 @@ paths: tags: - Auth summary: 删除用户 - description: 需要 users:manage 权限。 + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5946,7 +5949,8 @@ paths: tags: - Auth summary: 重置用户密码 - description: 需要 users:manage 权限。 + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7113,7 +7117,10 @@ paths: - Roles summary: 创建自定义角色 description: | - 创建一个具有指定权限的新自定义角色。角色名称必须为 2-30 个字符,小写字母数字,可含连字符和下划线。需要 security:manage 权限。 + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7143,7 +7150,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7196,7 +7204,10 @@ paths: - Roles summary: 更新自定义角色 description: | - 更新自定义角色的名称、描述或权限。内置角色无法修改。需要 security:manage 权限。 + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7270,7 +7281,10 @@ paths: - Roles summary: 删除自定义角色 description: | - 删除自定义角色。分配到此角色的用户会被重新分配到默认的 "user" 角色。内置角色无法删除。需要 security:manage 权限。 + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12043,6 +12057,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17719,7 +17787,8 @@ paths: tags: - Auth summary: 重置用户 MFA - description: 管理员重置用户的 MFA 注册。需要 users:manage 权限。 + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18045,7 +18114,8 @@ paths: tags: - Enterprise summary: 创建 SCIM token - description: 生成 SCIM bearer token。需要 users:manage 权限和企业版 SCIM 功能。 + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18058,7 +18128,8 @@ paths: tags: - Enterprise summary: 吊销 SCIM token - description: 吊销当前的 SCIM bearer token。需要 users:manage 权限和企业版 SCIM 功能。 + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18265,7 +18336,8 @@ paths: tags: - Enterprise summary: 导入配置 - description: 导入实例配置、自定义角色和团队。需要 system:health 权限。 + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18509,7 +18581,8 @@ paths: tags: - Enterprise summary: 清除用户数据 - description: 在明确确认后永久清除用户的数据。需要 compliance:manage 权限。 + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18523,6 +18596,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18531,7 +18606,8 @@ paths: tags: - Enterprise summary: 清除团队数据 - description: 在明确确认后永久清除团队的数据。需要 compliance:manage 权限。 + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18545,6 +18621,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19470,34 +19548,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 37c825ff40e7 - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d4815d01f763 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: bf2f0ceb697f - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d4815d01f763 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: b75872bba6f1 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d4815d01f763 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 69d844482cde - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: d4815d01f763 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19662,26 +19724,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: ffac9ecd3ca8 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: d3d922d948a5 paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: b78256265725 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 4cce096d145b paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: c387d4e83e8f - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: b51b5bb8c33f paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21262,10 +21312,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: dcaa9fbbca15 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: 4b2ce7c5fb00 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21390,18 +21436,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: 94bc0fdf379b - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: b176ec91d961 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: 1a9c4cdf27be - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 12df23f29f1d paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21526,10 +21564,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: b48b6518293a - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: 508f6a7a6c20 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21638,18 +21672,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: b1993816ecc6 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: af2bd8220307 paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: b050485650bc - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: 210e5a96c375 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/openapi.zh-TW.yaml b/apps/api/src/openapi.zh-TW.yaml index ab434027..81d9f016 100644 --- a/apps/api/src/openapi.zh-TW.yaml +++ b/apps/api/src/openapi.zh-TW.yaml @@ -5782,7 +5782,8 @@ paths: tags: - Auth summary: 建立使用者 - description: 需要 users:manage 權限。 + description: | + Requires users:manage permission. The assigned role must be within the actor's effective role authority, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] requestBody: @@ -5853,7 +5854,8 @@ paths: tags: - Auth summary: 更新使用者 - description: 需要 users:manage 權限。 + description: | + Requires users:manage permission and authority over the target's current role. Any newly assigned role must also be within the actor's effective hierarchy, permissions, tool access, and API-key scope. security: - bearerAuth: [] parameters: @@ -5908,7 +5910,8 @@ paths: tags: - Auth summary: 刪除使用者 - description: 需要 users:manage 權限。 + description: | + Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. Self-deletion is not allowed. security: - bearerAuth: [] parameters: @@ -5946,7 +5949,8 @@ paths: tags: - Auth summary: 重設使用者密碼 - description: 需要 users:manage 權限。 + description: | + Reset a local user's password. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -7113,7 +7117,10 @@ paths: - Roles summary: 建立自訂角色 description: | - 以指定權限建立新的自訂角色。角色名稱須為 2 至 30 個字元,小寫英數字並可含連字號與底線。需要 security:manage 權限。 + Create a new custom role with specified permissions. Role names must be + 2-30 characters, lowercase alphanumeric with hyphens and underscores. + Requires security:manage permission. The new role's permissions and tool + access must be contained by the actor's effective authority. security: - bearerAuth: [] requestBody: @@ -7143,7 +7150,8 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - features:manage, system:health, audit:read, security:manage + features:manage, system:health, audit:read, compliance:manage, + webhooks:manage, security:manage responses: '201': description: Role created @@ -7196,7 +7204,10 @@ paths: - Roles summary: 更新自訂角色 description: | - 更新自訂角色的名稱、說明或權限。內建角色無法修改。需要 security:manage 權限。 + Update name, description, or permissions of a custom role. Built-in + roles cannot be modified. Requires security:manage permission and + authority over the current role; the resulting permissions and tool + access must remain contained by the actor's effective authority. security: - bearerAuth: [] parameters: @@ -7270,7 +7281,10 @@ paths: - Roles summary: 刪除自訂角色 description: | - 刪除自訂角色。指派至此角色的使用者會被重新指派至預設的「user」角色。內建角色無法刪除。需要 security:manage 權限。 + Delete a custom role. Users assigned to this role are reassigned to the + default "user" role. Built-in roles cannot be deleted. Requires + security:manage permission, authority over the current role, and + authority to assign the fallback role. security: - bearerAuth: [] parameters: @@ -12043,6 +12057,60 @@ paths: application/json: schema: $ref: '#/components/schemas/UnauthorizedError' + /api/v1/tools/image/rounded-crop: + post: + operationId: roundedCrop + tags: + - Tools + summary: Rounded crop + description: Crop an image to a rounded square or squircle with transparent corners, optional border, and adjustable zoom/offset. + security: + - bearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: Image file to process + settings: + type: string + description: | + JSON string with options: + - `shape` (string, default "rounded-square") - "rounded-square" or "squircle" + - `cornerRadius` (number 0-50, default 25) - Corner radius as a percent of the shorter side (rounded-square only) + - `zoom` (number 1-5, default 1) - Zoom factor (>=1 crops tighter) + - `offsetX` (number 0-1, default 0.5) - Horizontal center position + - `offsetY` (number 0-1, default 0.5) - Vertical center position + - `borderWidth` (integer 0-200, default 0) - Border width in pixels + - `borderColor` (string, default "#ffffff") - Border hex color + - `background` (string, default "transparent") - "transparent" or a hex color for corners + - `outputSize` (integer 16-4096, optional) - Final square dimension in pixels + responses: + '200': + description: Processed image + content: + application/json: + schema: + $ref: '#/components/schemas/ToolResponse' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/UnauthorizedError' /api/v1/tools/image/duotone: post: operationId: duotone @@ -17719,7 +17787,8 @@ paths: tags: - Auth summary: 重設使用者 MFA - description: 管理員重設某使用者的 MFA 註冊。需要 users:manage 權限。 + description: | + Reset a user's MFA enrollment. Requires users:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18045,7 +18114,8 @@ paths: tags: - Enterprise summary: 建立 SCIM token - description: 產生 SCIM bearer token。需要 users:manage 權限與企業 SCIM 功能。 + description: | + Generate a global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. Legacy unversioned tokens are invalid and must be reissued as v2 tokens. security: - bearerAuth: [] responses: @@ -18058,7 +18128,8 @@ paths: tags: - Enterprise summary: 撤銷 SCIM token - description: 撤銷目前的 SCIM bearer token。需要 users:manage 權限與企業 SCIM 功能。 + description: | + Revoke the global SCIM bearer token. Requires the enterprise SCIM feature and full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18265,7 +18336,8 @@ paths: tags: - Enterprise summary: 匯入設定 - description: 匯入執行個體設定、自訂角色與團隊。需要 system:health 權限。 + description: | + Import instance configuration, custom roles, and teams. Requires full effective built-in administrator authority; custom roles and API keys missing any administrator permission do not qualify. security: - bearerAuth: [] responses: @@ -18509,7 +18581,8 @@ paths: tags: - Enterprise summary: 清除使用者資料 - description: 在明確確認後永久清除某使用者的資料。需要 compliance:manage 權限。 + description: | + Permanently purge a user's data after explicit confirmation. Requires compliance:manage permission and authority over the target's current role, including hierarchy, permissions, tool access, and any API-key scope. security: - bearerAuth: [] parameters: @@ -18523,6 +18596,8 @@ paths: description: User purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or target role beyond actor authority '409': description: User or team is under legal hold /api/v1/enterprise/teams/{id}/purge: @@ -18531,7 +18606,8 @@ paths: tags: - Enterprise summary: 清除團隊資料 - description: 在明確確認後永久清除某團隊的資料。需要 compliance:manage 權限。 + description: | + Permanently purge a team's data after explicit confirmation. Requires compliance:manage permission and authority over every member's current role, including hierarchy, permissions, tool access, and any API-key scope. A team containing the actor cannot be purged. security: - bearerAuth: [] parameters: @@ -18545,6 +18621,8 @@ paths: description: Team purged '400': description: Confirmation required + '403': + description: Feature not licensed, insufficient permission, or a member role beyond actor authority '409': description: Team is under legal hold /api/v1/admin/version: @@ -19470,34 +19548,18 @@ x-i18n: sourceHash: f06da128c342 provenance: machine outputHash: 6bf03b2bea3d - paths./api/auth/register.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 82f8753ac9c9 paths./api/auth/users/{id}.put.summary: sourceHash: 602b8b1607b0 provenance: machine outputHash: 9d72b3258b5f - paths./api/auth/users/{id}.put.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 82f8753ac9c9 paths./api/auth/users/{id}.delete.summary: sourceHash: 58363fd94240 provenance: machine outputHash: 2f145133aee2 - paths./api/auth/users/{id}.delete.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 82f8753ac9c9 paths./api/auth/users/{id}/reset-password.post.summary: sourceHash: aae899c2d3ad provenance: machine outputHash: 1ddd897b4547 - paths./api/auth/users/{id}/reset-password.post.description: - sourceHash: 34d5a11733bd - provenance: machine - outputHash: 82f8753ac9c9 paths./api/v1/api-keys.get.summary: sourceHash: ad0527749f03 provenance: machine @@ -19662,26 +19724,14 @@ x-i18n: sourceHash: 6dd06b2529d4 provenance: machine outputHash: bd336c413d74 - paths./api/v1/roles.post.description: - sourceHash: d740216038e7 - provenance: machine - outputHash: a98b8935c01b paths./api/v1/roles/{id}.put.summary: sourceHash: 6c0cda1bc967 provenance: machine outputHash: 2ed38e477b15 - paths./api/v1/roles/{id}.put.description: - sourceHash: e06c42a9c3ed - provenance: machine - outputHash: 10ef82dc5ef2 paths./api/v1/roles/{id}.delete.summary: sourceHash: d5a3ab01055a provenance: machine outputHash: a6a9dc2fa777 - paths./api/v1/roles/{id}.delete.description: - sourceHash: b4c1c8c0c9e0 - provenance: machine - outputHash: 87277f3e9047 paths./api/v1/tools/video/convert-video.post.summary: sourceHash: b4defe1d6906 provenance: machine @@ -21262,10 +21312,6 @@ x-i18n: sourceHash: c765204eb72c provenance: machine outputHash: ea61f9c66067 - paths./api/auth/users/{id}/mfa/reset.post.description: - sourceHash: 9d650aeb7d10 - provenance: machine - outputHash: bfb2d9a71cc9 paths./api/auth/oidc/login.get.summary: sourceHash: bac046b3ef24 provenance: machine @@ -21390,18 +21436,10 @@ x-i18n: sourceHash: e550c4aaeecf provenance: machine outputHash: f5586b9ae7f7 - paths./api/v1/enterprise/scim/token.post.description: - sourceHash: 39d3b5d5066b - provenance: machine - outputHash: bda5b7dbe629 paths./api/v1/enterprise/scim/token.delete.summary: sourceHash: e57912d50189 provenance: machine outputHash: bc0a33556fb8 - paths./api/v1/enterprise/scim/token.delete.description: - sourceHash: 412b38ac0141 - provenance: machine - outputHash: 9f1c85e7cc76 paths./api/v1/scim/v2/ServiceProviderConfig.get.summary: sourceHash: 6d9bb054fb7b provenance: machine @@ -21526,10 +21564,6 @@ x-i18n: sourceHash: 8a507f40b80f provenance: machine outputHash: f5475897e4a5 - paths./api/v1/enterprise/config/import.post.description: - sourceHash: cf4809a9459d - provenance: machine - outputHash: ef3016ad3dc7 paths./api/v1/enterprise/ip-allowlist.get.summary: sourceHash: 8756df674b27 provenance: machine @@ -21638,18 +21672,10 @@ x-i18n: sourceHash: 09c40d574cc0 provenance: machine outputHash: 4656d958bfc7 - paths./api/v1/enterprise/users/{id}/purge.delete.description: - sourceHash: 6db9ed4dabf1 - provenance: machine - outputHash: 4058985ee80b paths./api/v1/enterprise/teams/{id}/purge.delete.summary: sourceHash: e5f557c6db8b provenance: machine outputHash: e333ae2d8607 - paths./api/v1/enterprise/teams/{id}/purge.delete.description: - sourceHash: 5bcd69dee315 - provenance: machine - outputHash: bd5d036e75a5 paths./api/v1/admin/version.get.summary: sourceHash: 1fa5f3f5fa4a provenance: machine diff --git a/apps/api/src/permissions.ts b/apps/api/src/permissions.ts index c58f670e..5b106606 100644 --- a/apps/api/src/permissions.ts +++ b/apps/api/src/permissions.ts @@ -36,23 +36,72 @@ const ROLE_PERMISSIONS: Record = { user: ["tools:use", "files:own", "apikeys:own", "pipelines:own", "settings:read"], }; -export async function getPermissions(role: Role | string): Promise { - if (typeof role !== "string" || isDisabledRole(role)) return []; +export const ROLE_HIERARCHY: Record = { + admin: 3, + editor: 2, + user: 1, +}; + +export interface RoleToolPermissions { + mode: string; + allowed: string[]; +} + +interface ValidRoleToolPermissions extends RoleToolPermissions { + mode: "category" | "tool"; +} + +interface RoleDefinition { + permissions: Permission[]; + toolPermissions: ValidRoleToolPermissions | null; +} + +function isValidRoleToolPermissions(value: unknown): value is ValidRoleToolPermissions | null { + if (value === null) return true; + if (typeof value !== "object" || Array.isArray(value)) return false; + + const candidate = value as Record; + return ( + (candidate.mode === "category" || candidate.mode === "tool") && + Array.isArray(candidate.allowed) && + candidate.allowed.every((allowed) => typeof allowed === "string") + ); +} + +async function getRoleDefinition(role: string): Promise { + if (isDisabledRole(role)) return null; if (role in ROLE_PERMISSIONS) { - return ROLE_PERMISSIONS[role as Role]; + return { + permissions: ROLE_PERMISSIONS[role as Role], + toolPermissions: null, + }; } + try { const [customRole] = await db - .select() + .select({ + permissions: schema.roles.permissions, + toolPermissions: schema.roles.toolPermissions, + }) .from(schema.roles) - .where(eq(schema.roles.name, role as string)); - if (customRole) { - return customRole.permissions as Permission[]; - } + .where(eq(schema.roles.name, role)) + .limit(1); + if (!customRole) return null; + if (!isValidRoleToolPermissions(customRole.toolPermissions)) return null; + + return { + permissions: customRole.permissions as Permission[], + toolPermissions: customRole.toolPermissions, + }; } catch { - // DB not yet available during early startup + // Fail closed when role data is unavailable. + return null; } - return []; +} + +export async function getPermissions(role: Role | string): Promise { + if (typeof role !== "string") return []; + return (await getRoleDefinition(role))?.permissions ?? []; } export async function hasPermission(role: Role | string, permission: Permission): Promise { @@ -92,13 +141,194 @@ export async function permissionsNotHeldBy( ); } +function getRoleLevel(role: string): number { + return ROLE_HIERARCHY[role as Role] ?? 0; +} + +function normalizeManagedRole(role: string): string { + let managedRole = role; + while (managedRole.startsWith("disabled:")) { + managedRole = managedRole.slice("disabled:".length); + } + + // Legacy bare markers and malformed empty/nested markers have lost their + // original authority context. Treat them as admin so target checks fail + // closed for every non-admin actor. + return managedRole && managedRole !== "disabled" ? managedRole : "admin"; +} + +async function isPerToolPermissionEnforced(): Promise { + try { + const { isFeatureEnabled } = await import("@snapotter/enterprise"); + return isFeatureEnabled("per_tool_permissions"); + } catch { + return false; + } +} + +async function toolPermissionAllows( + toolPermissions: RoleToolPermissions | null, + toolId: string, + perToolPermissionEnforced: boolean, +): Promise { + if (!toolPermissions) return true; + + if (toolPermissions.mode === "category") { + const { TOOLS } = await import("@snapotter/shared"); + const tool = TOOLS.find((candidate) => candidate.id === toolId); + if (!tool) return false; + return toolPermissions.allowed.includes(tool.modality ?? tool.category); + } + + if (toolPermissions.mode === "tool") { + // Preserve the historical graceful degradation behavior: without the + // enterprise feature, per-tool restrictions behave as unrestricted. + if (!perToolPermissionEnforced) return true; + return toolPermissions.allowed.includes(toolId); + } + + return true; +} + +async function isToolScopeContained( + actorToolPermissions: RoleToolPermissions | null, + targetToolPermissions: RoleToolPermissions | null, +): Promise { + if (!actorToolPermissions) return true; + + const perToolPermissionEnforced = await isPerToolPermissionEnforced(); + if (actorToolPermissions.mode === "tool" && !perToolPermissionEnforced) return true; + if (!targetToolPermissions) return false; + if (targetToolPermissions.mode === "tool" && !perToolPermissionEnforced) return false; + + const actorMode = actorToolPermissions.mode; + const targetMode = targetToolPermissions.mode; + const validModes = new Set(["category", "tool"]); + if (!validModes.has(actorMode)) return true; + if (!validModes.has(targetMode)) return false; + + if (actorMode === targetMode) { + const actorAllowed = new Set(actorToolPermissions.allowed); + return targetToolPermissions.allowed.every((allowed) => actorAllowed.has(allowed)); + } + + // A category grant is open-ended: future tools in that category are also + // allowed. A finite per-tool allowlist therefore cannot contain it safely. + if (targetMode === "category") return false; + + const { TOOLS } = await import("@snapotter/shared"); + const actorAllowed = new Set(actorToolPermissions.allowed); + return targetToolPermissions.allowed.every((toolId) => { + const tool = TOOLS.find((candidate) => candidate.id === toolId); + return tool ? actorAllowed.has(tool.modality ?? tool.category) : false; + }); +} + +async function canControlRoleDefinition( + actor: AuthUser, + targetRole: string, + targetDefinition: RoleDefinition, +): Promise { + if (isDisabledRole(actor.role)) return false; + if (getRoleLevel(targetRole) > getRoleLevel(actor.role)) return false; + + const actorDefinition = await getRoleDefinition(actor.role); + if (!actorDefinition) return false; + + const actorPermissions = new Set( + actor.apiKeyPermissions + ? actorDefinition.permissions.filter((permission) => + actor.apiKeyPermissions?.includes(permission), + ) + : actorDefinition.permissions, + ); + if (!targetDefinition.permissions.every((permission) => actorPermissions.has(permission))) { + return false; + } + + if ( + targetDefinition.permissions.includes("tools:use") && + !(await isToolScopeContained(actorDefinition.toolPermissions, targetDefinition.toolPermissions)) + ) { + return false; + } + + return true; +} + export async function canAssignRole(actor: AuthUser, targetRole: string): Promise { if (isDisabledRole(targetRole)) return false; - const targetPermissions = await getPermissions(targetRole); - if (targetPermissions.length === 0) return false; + const targetDefinition = await getRoleDefinition(targetRole); + if (!targetDefinition || targetDefinition.permissions.length === 0) return false; - const actorPermissions = new Set(await getEffectivePermissions(actor)); - return targetPermissions.every((permission) => actorPermissions.has(permission)); + return canControlRoleDefinition(actor, targetRole, targetDefinition); +} + +export async function canManageTargetRole(actor: AuthUser, targetRole: string): Promise { + const managedRole = normalizeManagedRole(targetRole); + const targetDefinition = await getRoleDefinition(managedRole); + if (!targetDefinition) return false; + + return canManageRoleDefinition( + actor, + managedRole, + targetDefinition.permissions, + targetDefinition.toolPermissions, + ); +} + +export async function canManageRoleDefinition( + actor: AuthUser, + targetRole: string, + permissions: readonly string[], + toolPermissions: RoleToolPermissions | null, +): Promise { + if (!isValidRoleToolPermissions(toolPermissions)) return false; + return canControlRoleDefinition(actor, targetRole, { + permissions: permissions as Permission[], + toolPermissions, + }); +} + +export async function canGrantRoleDefinition( + actor: AuthUser, + permissions: readonly string[], + toolPermissions: RoleToolPermissions | null, +): Promise { + if (!isValidRoleToolPermissions(toolPermissions)) return false; + return canControlRoleDefinition(actor, "custom", { + permissions: permissions as Permission[], + toolPermissions, + }); +} + +/** + * Global authority is reserved for the built-in admin role with its complete + * effective permission set. API-key scopes are part of the containment check, + * so a key that omits any admin permission cannot cross this boundary. + */ +export async function isFullEffectiveAdmin(actor: AuthUser): Promise { + if (actor.role !== "admin") return false; + return canManageTargetRole(actor, "admin"); +} + +export async function requireFullAdmin( + request: FastifyRequest, + reply: FastifyReply, +): Promise { + const user = getAuthUser(request); + if (!user) { + reply.status(401).send({ error: "Authentication required", code: "AUTH_REQUIRED" }); + return null; + } + if (!(await isFullEffectiveAdmin(user))) { + reply.status(403).send({ + error: "Full administrator authority required", + code: "ESCALATION_DENIED", + }); + return null; + } + return user; } export function requirePermission( @@ -119,48 +349,15 @@ export function requirePermission( } export async function hasToolAccess(role: string, toolId: string): Promise { - if (isDisabledRole(role)) return false; + const roleDefinition = await getRoleDefinition(role); + if (!roleDefinition) return false; + if (!roleDefinition.toolPermissions) return true; - // Built-in roles have no tool restrictions - if (role in ROLE_PERMISSIONS) return true; - - try { - const [roleRow] = await db - .select({ toolPermissions: schema.roles.toolPermissions }) - .from(schema.roles) - .where(eq(schema.roles.name, role)) - .limit(1); - - // Unknown custom roles do not get implicit access. Known custom roles with - // no per-tool restriction keep the historical "all tools" behavior. - if (!roleRow) return false; - if (!roleRow.toolPermissions) return true; - - const tp = roleRow.toolPermissions; - - if (tp.mode === "category") { - const { TOOLS } = await import("@snapotter/shared"); - const tool = TOOLS.find((t) => t.id === toolId); - if (!tool) return false; - return tp.allowed.includes(tool.modality ?? tool.category); - } - - if (tp.mode === "tool") { - // Per-tool mode requires enterprise license - let isEnterprise = false; - try { - const { isFeatureEnabled } = await import("@snapotter/enterprise"); - isEnterprise = isFeatureEnabled("per_tool_permissions"); - } catch {} - - if (!isEnterprise) return true; // Graceful degradation -- no enterprise = allow all - return tp.allowed.includes(toolId); - } - - return true; // Unknown mode = allow - } catch { - return false; - } + return toolPermissionAllows( + roleDefinition.toolPermissions, + toolId, + roleDefinition.toolPermissions.mode === "tool" ? await isPerToolPermissionEnforced() : false, + ); } export async function hasEffectiveToolAccess(user: AuthUser, toolId: string): Promise { diff --git a/apps/api/src/plugins/auth.ts b/apps/api/src/plugins/auth.ts index edf32ecd..f7233993 100644 --- a/apps/api/src/plugins/auth.ts +++ b/apps/api/src/plugins/auth.ts @@ -13,6 +13,7 @@ import { authAttempts } from "../lib/metrics.js"; import { getSettingNumber, getSettingString } from "../lib/settings-helpers.js"; import { canAssignRole, + canManageTargetRole, getPermissions, isDisabledRole, requirePermission, @@ -760,19 +761,9 @@ export async function authRoutes(app: FastifyInstance): Promise { } } - // Escalation prevention - const roleHierarchy: Record = { admin: 3, editor: 2, user: 1 }; - const actorLevel = roleHierarchy[admin.role] ?? 0; - const targetLevel = roleHierarchy[role] ?? 0; - if (targetLevel > actorLevel) { - return reply.status(403).send({ - error: "Cannot create a user with a higher role than your own", - code: "ESCALATION_DENIED", - }); - } if (!(await canAssignRole(admin, role))) { return reply.status(403).send({ - error: "Cannot create a user with permissions you don't have", + error: "Cannot create a user beyond your role authority", code: "ESCALATION_DENIED", }); } @@ -880,6 +871,13 @@ export async function authRoutes(app: FastifyInstance): Promise { return reply.status(404).send({ error: "User not found", code: "NOT_FOUND" }); } + if (!(await canManageTargetRole(admin, user.role))) { + return reply.status(403).send({ + error: "Cannot manage a user beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + const updates: { role?: string; team?: string; updatedAt: Date } = { updatedAt: new Date(), }; @@ -891,18 +889,9 @@ export async function authRoutes(app: FastifyInstance): Promise { : await db.select().from(schema.roles).where(eq(schema.roles.name, body.role)); const isValid = validBuiltinRoles.includes(body.role) || customRoleRow; if (isValid) { - const roleHierarchy: Record = { admin: 3, editor: 2, user: 1 }; - const actorLevel = roleHierarchy[admin.role] ?? 0; - const targetLevel = roleHierarchy[body.role] ?? 0; - if (targetLevel > actorLevel) { - return reply.status(403).send({ - error: "Cannot assign a role higher than your own", - code: "ESCALATION_DENIED", - }); - } if (!(await canAssignRole(admin, body.role))) { return reply.status(403).send({ - error: "Cannot assign a role with permissions you don't have", + error: "Cannot assign a role beyond your role authority", code: "ESCALATION_DENIED", }); } @@ -1001,6 +990,13 @@ export async function authRoutes(app: FastifyInstance): Promise { return reply.status(404).send({ error: "User not found", code: "NOT_FOUND" }); } + if (!(await canManageTargetRole(admin, user.role))) { + return reply.status(403).send({ + error: "Cannot manage a user beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + if (!user.passwordHash) { return reply.status(400).send({ error: "Cannot reset password for OIDC user.", @@ -1053,6 +1049,13 @@ export async function authRoutes(app: FastifyInstance): Promise { return reply.status(404).send({ error: "User not found", code: "NOT_FOUND" }); } + if (!(await canManageTargetRole(admin, user.role))) { + return reply.status(403).send({ + error: "Cannot manage a user beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + // Delete associated sessions await db.delete(schema.sessions).where(eq(schema.sessions.userId, id)); diff --git a/apps/api/src/plugins/mfa.ts b/apps/api/src/plugins/mfa.ts index 314c657f..c86d588f 100644 --- a/apps/api/src/plugins/mfa.ts +++ b/apps/api/src/plugins/mfa.ts @@ -9,8 +9,13 @@ import { sharedRedis } from "../jobs/connection.js"; import { auditFromRequest } from "../lib/audit.js"; import { decrypt, encrypt } from "../lib/encryption.js"; import { getSettingString } from "../lib/settings-helpers.js"; -import { getPermissions, isDisabledRole } from "../permissions.js"; -import { createSessionToken, getAuthUser, requireAuth } from "./auth.js"; +import { + canManageTargetRole, + getPermissions, + isDisabledRole, + requirePermission, +} from "../permissions.js"; +import { createSessionToken, requireAuth } from "./auth.js"; // ── Constants ───────────────────────────────────────────────────── @@ -434,16 +439,8 @@ export async function registerMfa(app: FastifyInstance): Promise { app.post( "/api/auth/users/:id/mfa/reset", async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { - const admin = getAuthUser(request); - if (!admin) { - return reply.status(401).send({ error: "Authentication required", code: "AUTH_REQUIRED" }); - } - - // Check users:manage permission - const { hasEffectivePermission } = await import("../permissions.js"); - if (!(await hasEffectivePermission(admin, "users:manage"))) { - return reply.status(403).send({ error: "Insufficient permissions", code: "FORBIDDEN" }); - } + const admin = await requirePermission("users:manage")(request, reply); + if (!admin) return; const { id } = request.params; @@ -452,6 +449,13 @@ export async function registerMfa(app: FastifyInstance): Promise { return reply.status(404).send({ error: "User not found", code: "NOT_FOUND" }); } + if (!(await canManageTargetRole(admin, targetUser.role))) { + return reply.status(403).send({ + error: "Cannot manage a user beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + if (!targetUser.totpEnabled) { return reply.status(400).send({ error: "MFA is not enabled for this user", diff --git a/apps/api/src/routes/enterprise/config.ts b/apps/api/src/routes/enterprise/config.ts index e76b4ecf..f2a879a6 100644 --- a/apps/api/src/routes/enterprise/config.ts +++ b/apps/api/src/routes/enterprise/config.ts @@ -4,7 +4,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { db, schema } from "../../db/index.js"; import { auditFromRequest } from "../../lib/audit.js"; -import { requirePermission } from "../../permissions.js"; +import { requireFullAdmin, requirePermission } from "../../permissions.js"; const CONFIG_SCHEMA_VERSION = 1; @@ -22,21 +22,63 @@ const REDACTED_KEYS = new Set([ "webhook_destinations", ]); +const roleNameField = z + .string() + .transform((value) => value.trim().toLowerCase()) + .pipe( + z + .string() + .min(2, "Role name must be 2-30 characters") + .max(30, "Role name must be 2-30 characters") + .regex( + /^[a-z0-9_-]+$/, + "Role name can only contain lowercase letters, numbers, hyphens, and underscores", + ), + ); + +const permissionField = z.enum([ + "tools:use", + "files:own", + "files:all", + "apikeys:own", + "apikeys:all", + "pipelines:own", + "pipelines:all", + "settings:read", + "settings:write", + "users:manage", + "teams:manage", + "features:manage", + "system:health", + "audit:read", + "compliance:manage", + "webhooks:manage", + "security:manage", +]); + +const importedRoleSchema = z + .object({ + name: roleNameField, + description: z.string().max(500).optional(), + // Empty permission sets are valid for exported deny-all custom roles. + permissions: z.array(permissionField), + toolPermissions: z + .object({ + mode: z.enum(["category", "tool"]), + allowed: z.array(z.string()), + }) + .strict() + .nullable() + .optional(), + }) + .strict(); + const importSchema = z.object({ dryRun: z.boolean().default(false), config: z.object({ configSchemaVersion: z.number(), settings: z.record(z.string()).optional(), - roles: z - .array( - z.object({ - name: z.string(), - description: z.string().optional(), - permissions: z.array(z.string()), - toolPermissions: z.any().optional(), - }), - ) - .optional(), + roles: z.array(importedRoleSchema).optional(), teams: z .array( z.object({ @@ -125,7 +167,7 @@ export async function registerConfigRoutes(app: FastifyInstance): Promise app.post( "/api/v1/enterprise/config/import", async (request: FastifyRequest<{ Body: unknown }>, reply: FastifyReply) => { - const user = await requirePermission("system:health")(request, reply); + const user = await requireFullAdmin(request, reply); if (!user) return; // Enterprise feature gate diff --git a/apps/api/src/routes/enterprise/gdpr.ts b/apps/api/src/routes/enterprise/gdpr.ts index e4675e28..98493cd2 100644 --- a/apps/api/src/routes/enterprise/gdpr.ts +++ b/apps/api/src/routes/enterprise/gdpr.ts @@ -9,7 +9,7 @@ import { SYSTEM_JOBS } from "../../jobs/system-jobs.js"; import { auditFromRequest } from "../../lib/audit.js"; import { deleteStoredFile, deleteThumbnail } from "../../lib/file-storage.js"; import { deletePrefix } from "../../lib/object-storage.js"; -import { requirePermission } from "../../permissions.js"; +import { canManageTargetRole, requirePermission } from "../../permissions.js"; const purgeBodySchema = z.object({ confirm: z.literal(true), @@ -253,6 +253,7 @@ export async function registerGdprRoutes(app: FastifyInstance): Promise { const [targetUser] = await db .select({ id: schema.users.id, + role: schema.users.role, team: schema.users.team, legalHold: schema.users.legalHold, }) @@ -262,6 +263,13 @@ export async function registerGdprRoutes(app: FastifyInstance): Promise { return reply.status(404).send({ error: "User not found" }); } + if (!(await canManageTargetRole(user, targetUser.role))) { + return reply.status(403).send({ + error: "Cannot manage a user beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + // Check legal hold on user if (targetUser.legalHold) { return reply.status(409).send({ error: "User or team is under legal hold" }); @@ -341,10 +349,27 @@ export async function registerGdprRoutes(app: FastifyInstance): Promise { // Get all users in the team const teamUsers = await db - .select({ id: schema.users.id, legalHold: schema.users.legalHold }) + .select({ + id: schema.users.id, + role: schema.users.role, + legalHold: schema.users.legalHold, + }) .from(schema.users) .where(eq(schema.users.team, targetTeamId)); + if (teamUsers.some((member) => member.id === user.id)) { + return reply.status(400).send({ error: "Cannot purge a team containing your own account" }); + } + + for (const member of teamUsers) { + if (!(await canManageTargetRole(user, member.role))) { + return reply.status(403).send({ + error: "Cannot manage a user beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + } + // Check none of the users have individual legalHold const heldUser = teamUsers.find((u) => u.legalHold); if (heldUser) { diff --git a/apps/api/src/routes/enterprise/scim.ts b/apps/api/src/routes/enterprise/scim.ts index 3b84bc49..5c6e9357 100644 --- a/apps/api/src/routes/enterprise/scim.ts +++ b/apps/api/src/routes/enterprise/scim.ts @@ -1,13 +1,16 @@ import { randomBytes, randomUUID } from "node:crypto"; -import { and, eq } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { db, schema } from "../../db/index.js"; import { sharedRedis } from "../../jobs/connection.js"; import { auditLog } from "../../lib/audit.js"; import { getSettingString, upsertSetting } from "../../lib/settings-helpers.js"; -import { requirePermission } from "../../permissions.js"; +import { isDisabledRole, requireFullAdmin } from "../../permissions.js"; import { hashPassword, verifyPassword } from "../../plugins/auth.js"; +const SCIM_TOKEN_PREFIX = "so_scim_v2_"; +const SCIM_TOKEN_SUFFIX_PATTERN = /^[0-9a-f]{64}$/; + // ── SCIM Error Format ──────────────────────────────────────────── function scimError(status: number, detail: string) { @@ -18,6 +21,45 @@ function scimError(status: number, detail: string) { }; } +async function rejectLastActiveAdminDeactivation( + user: { role: string }, + reply: FastifyReply, +): Promise { + if (user.role !== "admin") return false; + + const [result] = await db + .select({ count: sql`COUNT(*)` }) + .from(schema.users) + .where(eq(schema.users.role, "admin")); + if (result && result.count <= 1) { + reply.status(409).send(scimError(409, "Cannot deactivate the last active administrator")); + return true; + } + + return false; +} + +function scimActiveValue(value: unknown): boolean { + return value === true || value === "true" || value === "True"; +} + +const DISABLED_ROLE_PREFIX = "disabled:"; + +function restoredScimRole(role: string): string { + let restoredRole = role; + while (restoredRole.startsWith(DISABLED_ROLE_PREFIX)) { + restoredRole = restoredRole.slice(DISABLED_ROLE_PREFIX.length); + } + + // Legacy bare or empty disabled markers did not retain an original role. + return restoredRole && restoredRole !== "disabled" ? restoredRole : "user"; +} + +function canonicalDisabledScimRole(role: string): string { + const activeRole = isDisabledRole(role) ? restoredScimRole(role) : role; + return `${DISABLED_ROLE_PREFIX}${activeRole || "user"}`; +} + // ── SCIM Bearer Token Auth ─────────────────────────────────────── async function scimAuth(request: FastifyRequest, reply: FastifyReply): Promise { @@ -28,6 +70,17 @@ async function scimAuth(request: FastifyRequest, reply: FastifyReply): Promise { app.post( "/api/v1/enterprise/scim/token", async (request: FastifyRequest, reply: FastifyReply) => { - const user = await requirePermission("users:manage")(request, reply); - if (!user) return; + if (!(await requireFullAdmin(request, reply))) return; if (!(await requireScimFeature(reply))) return; - const token = randomBytes(32).toString("hex"); + const token = `${SCIM_TOKEN_PREFIX}${randomBytes(32).toString("hex")}`; const hash = await hashPassword(token); await upsertSetting("scim_token_hash", hash); @@ -213,8 +265,7 @@ export async function registerScimRoutes(app: FastifyInstance): Promise { app.delete( "/api/v1/enterprise/scim/token", async (request: FastifyRequest, reply: FastifyReply) => { - const user = await requirePermission("users:manage")(request, reply); - if (!user) return; + if (!(await requireFullAdmin(request, reply))) return; if (!(await requireScimFeature(reply))) return; await db.delete(schema.settings).where(eq(schema.settings.key, "scim_token_hash")); @@ -503,6 +554,8 @@ export async function registerScimRoutes(app: FastifyInstance): Promise { const active = body.active !== false; const emails = body.emails as Array<{ value: string; primary?: boolean }> | undefined; + if (!active && (await rejectLastActiveAdminDeactivation(existing, reply))) return; + const updates: Record = { updatedAt: new Date() }; if (userName && userName !== existing.username) { @@ -527,14 +580,14 @@ export async function registerScimRoutes(app: FastifyInstance): Promise { } // Handle active/deactivation (preserve original role through disable/enable cycle) - if (active && existing.role.startsWith("disabled:")) { - updates.role = existing.role.slice("disabled:".length); - } else if (active && existing.role === "disabled") { - updates.role = "user"; // fallback when no previous role stored - } else if (!active && !existing.role.startsWith("disabled")) { - updates.role = `disabled:${existing.role}`; - // Revoke all sessions on deactivation - await db.delete(schema.sessions).where(eq(schema.sessions.userId, id)); + if (active && isDisabledRole(existing.role)) { + updates.role = restoredScimRole(existing.role); + } else if (!active) { + updates.role = canonicalDisabledScimRole(existing.role); + if (!isDisabledRole(existing.role)) { + // Revoke all sessions when transitioning from active to disabled. + await db.delete(schema.sessions).where(eq(schema.sessions.userId, id)); + } } await db.update(schema.users).set(updates).where(eq(schema.users.id, id)); @@ -586,6 +639,19 @@ export async function registerScimRoutes(app: FastifyInstance): Promise { }; const operations = body.Operations ?? []; + const deactivatesUser = operations.some((op) => { + const opType = op.op.toLowerCase(); + if (opType !== "replace" && opType !== "add") return false; + if (op.path === "active") return !scimActiveValue(op.value); + if (!op.path && typeof op.value === "object" && op.value !== null && "active" in op.value) { + return !scimActiveValue((op.value as Record).active); + } + return false; + }); + if (deactivatesUser && (await rejectLastActiveAdminDeactivation(existing, reply))) { + return; + } + const updates: Record = { updatedAt: new Date() }; for (const op of operations) { @@ -601,14 +667,14 @@ export async function registerScimRoutes(app: FastifyInstance): Promise { ) { const activeVal = op.path === "active" ? op.value : (op.value as Record).active; - const active = activeVal === true || activeVal === "true" || activeVal === "True"; - if (active && existing.role.startsWith("disabled:")) { - updates.role = existing.role.slice("disabled:".length); - } else if (active && existing.role === "disabled") { - updates.role = "user"; // fallback when no previous role stored - } else if (!active && !existing.role.startsWith("disabled")) { - updates.role = `disabled:${existing.role}`; - await db.delete(schema.sessions).where(eq(schema.sessions.userId, id)); + const active = scimActiveValue(activeVal); + if (active && isDisabledRole(existing.role)) { + updates.role = restoredScimRole(existing.role); + } else if (!active) { + updates.role = canonicalDisabledScimRole(existing.role); + if (!isDisabledRole(existing.role)) { + await db.delete(schema.sessions).where(eq(schema.sessions.userId, id)); + } } } @@ -683,11 +749,13 @@ export async function registerScimRoutes(app: FastifyInstance): Promise { return reply.status(404).send(scimError(404, "User not found")); } + if (await rejectLastActiveAdminDeactivation(user, reply)) return; + // Soft-delete: preserve original role so reactivation can restore it await db .update(schema.users) .set({ - role: `disabled:${user.role}`, + role: canonicalDisabledScimRole(user.role), passwordHash: null, updatedAt: new Date(), }) diff --git a/apps/api/src/routes/roles.ts b/apps/api/src/routes/roles.ts index dc31c668..f5d2e31f 100644 --- a/apps/api/src/routes/roles.ts +++ b/apps/api/src/routes/roles.ts @@ -5,7 +5,12 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { z } from "zod"; import { db, schema } from "../db/index.js"; import { auditFromRequest } from "../lib/audit.js"; -import { permissionsNotHeldBy, requirePermission } from "../permissions.js"; +import { + canAssignRole, + canGrantRoleDefinition, + canManageRoleDefinition, + requirePermission, +} from "../permissions.js"; const ALL_PERMISSIONS: Permission[] = [ "tools:use", @@ -114,10 +119,9 @@ export async function rolesRoutes(app: FastifyInstance): Promise { .status(400) .send({ error: `Invalid permissions: ${invalid.join(", ")}`, code: "VALIDATION_ERROR" }); } - const broader = await permissionsNotHeldBy(user, permissions); - if (broader.length > 0) { + if (!(await canGrantRoleDefinition(user, permissions, toolPermissions ?? null))) { return reply.status(403).send({ - error: `Cannot grant permissions you don't have: ${broader.join(", ")}`, + error: "Cannot create a role beyond your role authority", code: "ESCALATION_DENIED", }); } @@ -171,6 +175,14 @@ export async function rolesRoutes(app: FastifyInstance): Promise { .status(400) .send({ error: "Cannot modify built-in roles", code: "VALIDATION_ERROR" }); } + if ( + !(await canManageRoleDefinition(user, role.name, role.permissions, role.toolPermissions)) + ) { + return reply.status(403).send({ + error: "Cannot manage a role beyond your role authority", + code: "ESCALATION_DENIED", + }); + } const parsed = updateRoleSchema.safeParse(request.body); if (!parsed.success) { @@ -200,25 +212,38 @@ export async function rolesRoutes(app: FastifyInstance): Promise { code: "VALIDATION_ERROR", }); } - const broader = await permissionsNotHeldBy(user, body.permissions); - if (broader.length > 0) { - return reply.status(403).send({ - error: `Cannot grant permissions you don't have: ${broader.join(", ")}`, - code: "ESCALATION_DENIED", - }); - } updates.permissions = body.permissions; } + + const prospectivePermissions = body.permissions ?? role.permissions; + const prospectiveToolPermissions = + body.toolPermissions !== undefined ? (body.toolPermissions ?? null) : role.toolPermissions; + if ( + !(await canGrantRoleDefinition(user, prospectivePermissions, prospectiveToolPermissions)) + ) { + return reply.status(403).send({ + error: "Cannot update a role beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + if (body.toolPermissions !== undefined) { updates.toolPermissions = body.toolPermissions ?? null; } await db.transaction(async (tx) => { if (body.name) { + const renamedDisabledRole = `disabled:${body.name}`; await tx .update(schema.users) - .set({ role: body.name }) - .where(eq(schema.users.role, role.name)); + .set({ + role: sql`CASE + WHEN ${schema.users.role} = ${role.name} THEN ${body.name} + ELSE ${renamedDisabledRole} + END`, + updatedAt: new Date(), + }) + .where(sql`regexp_replace(${schema.users.role}, '^(disabled:)+', '') = ${role.name}`); } await tx.update(schema.roles).set(updates).where(eq(schema.roles.id, id)); }); @@ -245,12 +270,36 @@ export async function rolesRoutes(app: FastifyInstance): Promise { .status(400) .send({ error: "Cannot delete built-in roles", code: "VALIDATION_ERROR" }); } + if ( + !(await canManageRoleDefinition(user, role.name, role.permissions, role.toolPermissions)) + ) { + return reply.status(403).send({ + error: "Cannot manage a role beyond your role authority", + code: "ESCALATION_DENIED", + }); + } + + // Deletion always has the side effect of assigning the built-in user + // fallback. Authorize that transition up front, even when the role is + // currently empty, so concurrent membership changes cannot bypass it. + if (!(await canAssignRole(user, "user"))) { + return reply.status(403).send({ + error: "Cannot reassign role members beyond your role authority", + code: "ESCALATION_DENIED", + }); + } await db.transaction(async (tx) => { await tx .update(schema.users) - .set({ role: "user", updatedAt: new Date() }) - .where(eq(schema.users.role, role.name)); + .set({ + role: sql`CASE + WHEN ${schema.users.role} = ${role.name} THEN 'user' + ELSE 'disabled:user' + END`, + updatedAt: new Date(), + }) + .where(sql`regexp_replace(${schema.users.role}, '^(disabled:)+', '') = ${role.name}`); await tx.delete(schema.roles).where(eq(schema.roles.id, id)); }); await auditFromRequest(request)("ROLE_DELETED", { diff --git a/apps/docs/api/rest.md b/apps/docs/api/rest.md index 3e4d684b..7c8a07f4 100644 --- a/apps/docs/api/rest.md +++ b/apps/docs/api/rest.md @@ -57,16 +57,16 @@ Keys are prefixed `si_` and stored as scrypt hashes - the raw key is shown once | `GET` | `/api/auth/session` | Auth | Validate current session | | `POST` | `/api/auth/change-password` | Auth | Change own password (invalidates all other sessions + API keys) | | `GET` | `/api/auth/users` | Admin | List all users | -| `POST` | `/api/auth/register` | Admin | Create a new user | -| `PUT` | `/api/auth/users/:id` | Admin | Update user role or team | -| `POST` | `/api/auth/users/:id/reset-password` | Admin | Reset user's password | -| `DELETE` | `/api/auth/users/:id` | Admin | Delete a user | +| `POST` | `/api/auth/register` | Admin (`users:manage`; proposed-role authority) | Create a new user | +| `PUT` | `/api/auth/users/:id` | Admin (`users:manage`; target authority) | Update user role or team | +| `POST` | `/api/auth/users/:id/reset-password` | Admin (`users:manage`; target authority) | Reset user's password | +| `DELETE` | `/api/auth/users/:id` | Admin (`users:manage`; target authority) | Delete a user | | `GET` | `/api/v1/config/auth` | Public | Check if authentication is enabled (`{ authEnabled: bool }`) | | `POST` | `/api/auth/mfa/enroll` | Auth | Start TOTP MFA enrollment. Requires the enterprise `mfa` feature | | `POST` | `/api/auth/mfa/verify` | Auth | Confirm MFA enrollment with a TOTP code | | `POST` | `/api/auth/mfa/complete` | Public | Complete a pending MFA login challenge | | `POST` | `/api/auth/mfa/disable` | Auth | Disable MFA for the current user | -| `POST` | `/api/auth/users/:id/mfa/reset` | Admin (`users:manage`) | Reset MFA for a user | +| `POST` | `/api/auth/users/:id/mfa/reset` | Admin (`users:manage`; target authority) | Reset MFA for a user | | `GET` | `/api/auth/oidc/login` | Public | Start OIDC login when OIDC is enabled | | `GET` | `/api/auth/oidc/callback` | Public | OIDC authorization callback | | `GET` | `/api/auth/saml/metadata` | Public | SAML SP metadata XML when SAML is enabled | @@ -553,6 +553,8 @@ Per-user preferences are separate from instance settings. Any authenticated user Custom role management with granular permissions. +Role creation and mutation are constrained by authority containment: the proposed or current role cannot outrank the actor, exceed the actor's effective permissions, or broaden the actor's tool scope. API-key scopes participate in this check. Deleting a custom role also requires authority to assign the built-in `user` fallback used for its members. + | Method | Path | Access | Description | |--------|------|--------|-------------| | `GET` | `/api/v1/roles` | Admin (`audit:read`) | List all roles with user counts | @@ -633,17 +635,19 @@ Operational endpoints for observability, support, usage reporting, and backup st These routes are license-gated by their related enterprise feature. They still require the listed SnapOtter permission. +**Full built-in admin** means the authenticated actor has the `admin` role and the complete effective admin permission set. An API-key scope that omits any admin permission does not qualify. + | Method | Path | Access | Description | |--------|------|--------|-------------| | `GET` | `/api/v1/enterprise/audit/export` | Admin (`audit:read`) | Export audit entries as JSON or CSV with filters | | `GET` | `/api/v1/enterprise/config/export` | Admin (`system:health`) | Export redacted instance config, custom roles, and teams | -| `POST` | `/api/v1/enterprise/config/import` | Admin (`system:health`) | Import config, with optional dry run | +| `POST` | `/api/v1/enterprise/config/import` | Full built-in admin | Import config, with optional dry run | | `GET` | `/api/v1/enterprise/ip-allowlist` | Admin (`security:manage`) | Read configured CIDR allowlist | | `PUT` | `/api/v1/enterprise/ip-allowlist` | Admin (`security:manage`) | Update CIDR allowlist with self-lockout prevention | | `GET` | `/api/v1/enterprise/legal-hold` | Admin (`compliance:manage`) | List user and team legal holds | | `PUT` | `/api/v1/enterprise/legal-hold` | Admin (`compliance:manage`) | Apply or release a legal hold on a user or team | -| `POST` | `/api/v1/enterprise/scim/token` | Admin (`users:manage`) | Generate a SCIM bearer token, returned once | -| `DELETE` | `/api/v1/enterprise/scim/token` | Admin (`users:manage`) | Revoke the current SCIM bearer token | +| `POST` | `/api/v1/enterprise/scim/token` | Full built-in admin | Generate a SCIM bearer token, returned once | +| `DELETE` | `/api/v1/enterprise/scim/token` | Full built-in admin | Revoke the current SCIM bearer token | | `GET` | `/api/v1/enterprise/siem/config` | Admin (`webhooks:manage`) | Read SIEM forwarding config | | `PUT` | `/api/v1/enterprise/siem/config` | Admin (`webhooks:manage`) | Update SIEM forwarding config | | `GET` | `/api/v1/enterprise/webhooks` | Admin (`webhooks:manage`) | List webhook destinations | @@ -653,15 +657,15 @@ These routes are license-gated by their related enterprise feature. They still r | `POST` | `/api/v1/enterprise/webhooks/:index/test` | Admin (`webhooks:manage`) | Send a test webhook payload | | `POST` | `/api/v1/enterprise/users/:id/export` | Admin (`compliance:manage`) | Start a GDPR user export job | | `GET` | `/api/v1/enterprise/users/:id/export/:jobId` | Admin (`compliance:manage`) | Read GDPR export status and download URL | -| `DELETE` | `/api/v1/enterprise/users/:id/purge` | Admin (`compliance:manage`) | Permanently purge a user's data after confirmation | -| `DELETE` | `/api/v1/enterprise/teams/:id/purge` | Admin (`compliance:manage`) | Permanently purge a team's data after confirmation | +| `DELETE` | `/api/v1/enterprise/users/:id/purge` | Admin (`compliance:manage`; target authority) | Permanently purge a user's data after confirmation | +| `DELETE` | `/api/v1/enterprise/teams/:id/purge` | Admin (`compliance:manage`; all-member authority) | Permanently purge a team's data after confirmation | | `GET` | `/api/v1/admin/version` | Admin (`system:health`) | Read app, build, Node, and schema version metadata | | `GET` | `/api/v1/admin/migrations/pending` | Admin (`system:health`) | Compare packaged migrations with applied migrations | | `GET` | `/api/v1/admin/upgrade-check` | Admin (`system:health`) | Run upgrade readiness checks | ### SCIM 2.0 {#scim-2-0} -SCIM discovery endpoints are public. User and group endpoints require the SCIM bearer token generated above. +SCIM discovery endpoints are public. User and group endpoints require the SCIM bearer token generated above. Legacy unversioned tokens are invalid and must be reissued as `so_scim_v2_...` tokens by a full built-in admin. | Method | Path | Access | Description | |--------|------|--------|-------------| diff --git a/apps/docs/guide/scim.md b/apps/docs/guide/scim.md index 16624add..d717710d 100644 --- a/apps/docs/guide/scim.md +++ b/apps/docs/guide/scim.md @@ -14,7 +14,7 @@ SCIM provisioning requires an **enterprise** license with the `scim` feature. It - A running SnapOtter instance reachable at a public URL - An enterprise license key with the `scim` feature -- Admin access to SnapOtter (the `users:manage` permission is required to generate or revoke a SCIM token) +- A built-in SnapOtter `admin` account with its full effective permission set. A delegated custom role or an admin API key missing any admin permission cannot generate or revoke the global SCIM token. - Admin access to your identity provider's provisioning settings ## Quick start {#quick-start} @@ -31,7 +31,7 @@ The response contains the token. Save it immediately; it cannot be retrieved aga ```json { - "token": "a1b2c3d4e5f6...", + "token": "so_scim_v2_a1b2c3d4e5f6...", "message": "Save this token - it cannot be retrieved again" } ``` @@ -46,15 +46,19 @@ SCIM endpoints use a dedicated Bearer token, separate from user sessions and API ### Generating a token {#generating-a-token} -`POST /api/v1/enterprise/scim/token` generates a new SCIM token. This endpoint requires a valid session with the `users:manage` permission. +`POST /api/v1/enterprise/scim/token` generates a new SCIM token. Because the token can provision and mutate users across the instance, this endpoint requires the built-in `admin` role with the complete effective admin permission set. Holding `users:manage` in a custom role is not sufficient. The token is returned in plaintext exactly once. SnapOtter stores only a scrypt hash. If you lose the token, revoke it and generate a new one. Only one SCIM token is active at a time. Generating a new token replaces the previous one. +::: warning Token reissue after upgrade +Legacy unversioned SCIM tokens are rejected. After upgrading to a release that issues `so_scim_v2_...` tokens, generate a new token and update your identity provider before resuming provisioning. +::: + ### Revoking a token {#revoking-a-token} -`DELETE /api/v1/enterprise/scim/token` revokes the current SCIM token. This endpoint also requires `users:manage`. +`DELETE /api/v1/enterprise/scim/token` revokes the current SCIM token. It has the same full built-in admin requirement as token generation. ### Rate limiting {#rate-limiting} @@ -276,7 +280,7 @@ The SCIM request did not include an `Authorization: Bearer ` header. Chec ### 401 "Invalid token" {#_401-invalid-token} -The token does not match the stored hash. This happens if the token was revoked and regenerated. Update the token in your IdP's provisioning settings. +The token is malformed, uses the retired unversioned format, or does not match the stored hash. Generate a current `so_scim_v2_...` token and update the token in your IdP's provisioning settings. ### 401 "SCIM not configured" {#_401-scim-not-configured} diff --git a/apps/docs/guide/users-roles.md b/apps/docs/guide/users-roles.md index 2007f0c5..50011c66 100644 --- a/apps/docs/guide/users-roles.md +++ b/apps/docs/guide/users-roles.md @@ -79,12 +79,12 @@ All 17 permissions. Full control over the instance. | `pipelines:all` | View and manage all users' pipelines | | `settings:read` | View instance settings | | `settings:write` | Modify instance settings | -| `users:manage` | Create, update, and delete user accounts | +| `users:manage` | Create and manage user accounts within the actor's authority boundary | | `teams:manage` | Create, update, and delete teams | | `features:manage` | Install and manage AI feature bundles | | `system:health` | Access health and readiness endpoints | | `audit:read` | View the audit log and list roles | -| `compliance:manage` | Manage GDPR lifecycle and compliance features | +| `compliance:manage` | Manage GDPR lifecycle and compliance features; destructive user operations remain authority-bounded | | `webhooks:manage` | Configure outbound webhooks | | `security:manage` | Manage security settings (IP allowlist, SSO enforcement) | @@ -107,15 +107,17 @@ curl -X POST http://localhost:1349/api/v1/roles \ Role names must be 2-30 characters, lowercase alphanumeric with hyphens and underscores. -### Admin-reserved permissions {#admin-reserved-permissions} +### Delegated administration boundaries {#delegated-administration-boundaries} -Three permissions are reserved for built-in roles and cannot be assigned to custom roles: +All 17 permissions can be delegated through custom roles, but an administrative permission does not make that role equivalent to the built-in `admin` role. User mutations authorized by `users:manage`, destructive operations authorized by `compliance:manage`, and custom-role management authorized by `security:manage` are bounded by the actor's current authority: -- `compliance:manage` -- `webhooks:manage` -- `security:manage` +- Built-in roles follow `admin` > `editor` > `user`; custom roles are below built-in roles. +- The target's permissions must be contained by the actor's **effective** permissions. A scoped API key therefore cannot exercise permissions omitted from its scope. +- A target role's tool access must be contained by the actor's own tool access. +- A disabled account is checked against its original role when that role is recorded as `disabled:`. +- Deleting a custom role also requires authority to assign the built-in `user` fallback; disabled members remain disabled as `disabled:user`. -The roles API rejects any request that includes these permissions. Only the built-in `admin` role has access to them. +Global credentials and configuration are stricter: issuing or revoking the SCIM token and importing instance configuration require the built-in `admin` role with complete effective admin authority. ### Tool-level permissions {#tool-level-permissions} diff --git a/tests/integration/platform/config-export-import.test.ts b/tests/integration/platform/config-export-import.test.ts index 74cd05ed..a1ea23d0 100644 --- a/tests/integration/platform/config-export-import.test.ts +++ b/tests/integration/platform/config-export-import.test.ts @@ -3,6 +3,26 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { db, schema } from "../../../apps/api/src/db/index.js"; import { buildTestApp, loginAsAdmin, type TestApp } from "../test-server.js"; +const ADMIN_PERMISSIONS = [ + "tools:use", + "files:own", + "files:all", + "apikeys:own", + "apikeys:all", + "pipelines:own", + "pipelines:all", + "settings:read", + "settings:write", + "users:manage", + "teams:manage", + "features:manage", + "system:health", + "audit:read", + "compliance:manage", + "webhooks:manage", + "security:manage", +]; + let testApp: TestApp; let adminToken: string; @@ -201,7 +221,188 @@ describe("config import with enterprise license", () => { expect(res.statusCode).toBe(403); }); - it("dry-run mode returns changes without applying", async () => { + it("denies config import to a custom role even when it has every admin permission", async () => { + const suffix = Date.now().toString(36); + const roleName = `config-health-${suffix}`; + const username = `config-health-user-${suffix}`; + const settingKey = `configImportAuthzSentinel${suffix}`; + let roleId: string | undefined; + let userId: string | undefined; + + try { + const roleRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${licensedToken}` }, + payload: { name: roleName, permissions: ADMIN_PERMISSIONS }, + }); + if (roleRes.statusCode !== 201) { + throw new Error(`Failed to create config test role: ${roleRes.body}`); + } + roleId = JSON.parse(roleRes.body).id as string; + + const registerRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${licensedToken}` }, + payload: { username, password: "TestPass1", role: roleName }, + }); + if (registerRes.statusCode !== 201) { + throw new Error(`Failed to create config test user: ${registerRes.body}`); + } + userId = JSON.parse(registerRes.body).id as string; + + await db + .update(schema.users) + .set({ mustChangePassword: false }) + .where(eq(schema.users.id, userId)); + + const loginRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username, password: "TestPass1" }, + }); + const actorToken = JSON.parse(loginRes.body).token as string; + + const res = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/enterprise/config/import", + headers: { authorization: `Bearer ${actorToken}` }, + payload: { + dryRun: false, + config: { + configSchemaVersion: 1, + settings: { [settingKey]: "must-not-be-imported" }, + }, + }, + }); + + const body = JSON.parse(res.body); + const [importedSetting] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, settingKey)); + + expect.soft(res.statusCode).toBe(403); + expect.soft(body.code).toBe("ESCALATION_DENIED"); + expect(importedSetting).toBeUndefined(); + } finally { + await db.delete(schema.settings).where(eq(schema.settings.key, settingKey)); + if (userId) await db.delete(schema.users).where(eq(schema.users.id, userId)); + if (roleId) await db.delete(schema.roles).where(eq(schema.roles.id, roleId)); + } + }); + + it("denies config import through a permission-scoped built-in admin API key", async () => { + const suffix = Date.now().toString(36); + const settingKey = `configImportScopedKeySentinel${suffix}`; + const keyRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/api-keys", + headers: { authorization: `Bearer ${licensedToken}` }, + payload: { + name: `config-import-scoped-${suffix}`, + permissions: ["system:health"], + }, + }); + expect(keyRes.statusCode, keyRes.body).toBe(201); + const scopedKey = JSON.parse(keyRes.body).key as string; + + try { + const res = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/enterprise/config/import", + headers: { authorization: `Bearer ${scopedKey}` }, + payload: { + dryRun: false, + config: { + configSchemaVersion: 1, + settings: { [settingKey]: "must-not-be-imported" }, + }, + }, + }); + const [importedSetting] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, settingKey)); + + expect.soft(res.statusCode).toBe(403); + expect.soft(JSON.parse(res.body).code).toBe("ESCALATION_DENIED"); + expect(importedSetting).toBeUndefined(); + } finally { + await db.delete(schema.settings).where(eq(schema.settings.key, settingKey)); + } + }); + + it.each([ + { + caseName: "unknown permissions", + roleName: "config-invalid-permission", + role: { + name: "config-invalid-permission", + permissions: ["users:impersonate"], + }, + }, + { + caseName: "unknown tool permission modes", + roleName: "config-invalid-tool-mode", + role: { + name: "config-invalid-tool-mode", + permissions: ["tools:use"], + toolPermissions: { mode: "everything", allowed: ["compress-image"] }, + }, + }, + { + caseName: "invalid role names", + roleName: "INVALID ROLE!", + role: { + name: "INVALID ROLE!", + permissions: ["settings:read"], + }, + }, + ])("rejects $caseName before mutating any configuration", async ({ roleName, role }) => { + const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + const settingKey = `configInvalidRoleSentinel${suffix}`; + + try { + const res = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/enterprise/config/import", + headers: { authorization: `Bearer ${licensedToken}` }, + payload: { + dryRun: false, + config: { + configSchemaVersion: 1, + settings: { [settingKey]: "must-not-be-imported" }, + roles: [role], + }, + }, + }); + const [importedSetting] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, settingKey)); + const [importedRole] = await db + .select() + .from(schema.roles) + .where(eq(schema.roles.name, roleName)); + + expect.soft(res.statusCode, res.body).toBe(400); + expect.soft(JSON.parse(res.body).error).toBe("Invalid import payload"); + expect.soft(importedSetting).toBeUndefined(); + expect(importedRole).toBeUndefined(); + } finally { + await db.delete(schema.settings).where(eq(schema.settings.key, settingKey)); + await db.delete(schema.roles).where(eq(schema.roles.name, roleName)); + } + }); + + it("dry-run reports setting, role, and team changes without mutating them", async () => { + const suffix = Date.now().toString(36); + const settingKey = `configDryRunSetting${suffix}`; + const roleName = `config-dry-run-role-${suffix}`; + const teamName = `config-dry-run-team-${suffix}`; + const res = await licensedApp.app.inject({ method: "POST", url: "/api/v1/enterprise/config/import", @@ -210,7 +411,9 @@ describe("config import with enterprise license", () => { dryRun: true, config: { configSchemaVersion: 1, - settings: { testSetting: "hello" }, + settings: { [settingKey]: "hello" }, + roles: [{ name: roleName, permissions: ["settings:read"] }], + teams: [{ name: teamName }], }, }, }); @@ -218,11 +421,23 @@ describe("config import with enterprise license", () => { const body = JSON.parse(res.body); expect(body.dryRun).toBe(true); expect(body.changes).toBeDefined(); - expect(body.changes.settings).toBeGreaterThanOrEqual(1); + expect(body.changes).toEqual({ settings: 1, roles: 1, teams: 1 }); expect(body.details).toBeDefined(); expect(body.details.settings).toEqual( - expect.arrayContaining([expect.objectContaining({ key: "testSetting" })]), + expect.arrayContaining([expect.objectContaining({ key: settingKey, action: "create" })]), ); + expect(body.details.roles).toContainEqual({ name: roleName, action: "create" }); + expect(body.details.teams).toContainEqual({ name: teamName, action: "create" }); + + const [setting] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, settingKey)); + const [role] = await db.select().from(schema.roles).where(eq(schema.roles.name, roleName)); + const [team] = await db.select().from(schema.teams).where(eq(schema.teams.name, teamName)); + expect.soft(setting).toBeUndefined(); + expect.soft(role).toBeUndefined(); + expect(team).toBeUndefined(); }); it("rejects future schema versions", async () => { @@ -240,7 +455,7 @@ describe("config import with enterprise license", () => { expect(body.error).toContain("Unsupported config schema version"); }); - it("empty config import succeeds with no changes", async () => { + it("allows the full built-in admin to import an empty config", async () => { const res = await licensedApp.app.inject({ method: "POST", url: "/api/v1/enterprise/config/import", diff --git a/tests/integration/platform/custom-roles-edge-cases.test.ts b/tests/integration/platform/custom-roles-edge-cases.test.ts index 7e02f08c..02091eef 100644 --- a/tests/integration/platform/custom-roles-edge-cases.test.ts +++ b/tests/integration/platform/custom-roles-edge-cases.test.ts @@ -5,6 +5,35 @@ import { buildTestApp, loginAsAdmin, type TestApp } from "../test-server.js"; let testApp: TestApp; let adminToken: string; +let userSequence = 0; + +const BASE_USER_PERMISSIONS = [ + "tools:use", + "files:own", + "apikeys:own", + "pipelines:own", + "settings:read", +]; + +const ADMIN_PERMISSIONS = [ + "tools:use", + "files:own", + "files:all", + "apikeys:own", + "apikeys:all", + "pipelines:own", + "pipelines:all", + "settings:read", + "settings:write", + "users:manage", + "teams:manage", + "features:manage", + "system:health", + "audit:read", + "compliance:manage", + "webhooks:manage", + "security:manage", +]; beforeAll(async () => { testApp = await buildTestApp(); @@ -44,12 +73,17 @@ async function createUserAndLogin( password: string, role: string, ): Promise { - await testApp.app.inject({ + const registerRes = await testApp.app.inject({ method: "POST", url: "/api/auth/register", headers: { authorization: `Bearer ${adminToken}` }, payload: { username, password, role }, }); + if (registerRes.statusCode !== 201) { + throw new Error( + `createUserAndLogin registration failed (${registerRes.statusCode}): ${registerRes.body}`, + ); + } await db .update(schema.users) .set({ mustChangePassword: false }) @@ -63,6 +97,47 @@ async function createUserAndLogin( return JSON.parse(loginRes.body).token as string; } +async function createTargetUser( + prefix: string, + role: string, + options: { mfaEnabled?: boolean } = {}, +): Promise<{ id: string; password: string; username: string }> { + userSequence += 1; + const username = `${prefix}-${Date.now()}-${userSequence}`; + const password = "TargetPass1"; + const registerRes = await testApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${adminToken}` }, + payload: { username, password, role }, + }); + if (registerRes.statusCode !== 201) { + throw new Error(`createTargetUser failed (${registerRes.statusCode}): ${registerRes.body}`); + } + + const id = JSON.parse(registerRes.body).id as string; + await db + .update(schema.users) + .set({ + mustChangePassword: false, + ...(options.mfaEnabled + ? { + totpEnabled: true, + totpSecret: "target-authority-test-secret", + recoveryCodesHash: "target-authority-test-recovery-codes", + } + : {}), + }) + .where(eq(schema.users.id, id)); + + return { id, password, username }; +} + +async function getUserById(id: string) { + const [user] = await db.select().from(schema.users).where(eq(schema.users.id, id)); + return user; +} + // --------------------------------------------------------------------------- // Name validation (5 tests) // --------------------------------------------------------------------------- @@ -278,3 +353,325 @@ describe("functional permissions", () => { expect(auditRes.statusCode).toBe(403); }); }); + +// --------------------------------------------------------------------------- +// Target role authority +// --------------------------------------------------------------------------- +describe("target role authority", () => { + let managerRole: string; + let subordinateRole: string; + let alternateSubordinateRole: string; + let managerToken: string; + let fullPermissionManagerToken: string; + + beforeAll(async () => { + const suffix = Date.now(); + managerRole = `mgr-${suffix}`; + subordinateRole = `sub-${suffix}`; + alternateSubordinateRole = `alt-${suffix}`; + + await createRole(managerRole, [...BASE_USER_PERMISSIONS, "users:manage"]); + await createRole(subordinateRole, BASE_USER_PERMISSIONS); + await createRole(alternateSubordinateRole, BASE_USER_PERMISSIONS); + managerToken = await createUserAndLogin(`manager-${suffix}`, "ManagerPass1", managerRole); + + const fullPermissionManagerRole = `full-mgr-${suffix}`; + await createRole(fullPermissionManagerRole, ADMIN_PERMISSIONS); + fullPermissionManagerToken = await createUserAndLogin( + `full-manager-${suffix}`, + "FullManagerPass1", + fullPermissionManagerRole, + ); + }); + + it("denies a custom role with all 17 admin permissions from managing built-in admins", async () => { + expect(ADMIN_PERMISSIONS).toHaveLength(17); + + const demotionTarget = await createTargetUser("full-deny-demote-admin", "admin"); + const passwordTarget = await createTargetUser("full-deny-reset-admin", "admin"); + const deleteTarget = await createTargetUser("full-deny-delete-admin", "admin"); + const mfaTarget = await createTargetUser("full-deny-mfa-admin", "admin", { + mfaEnabled: true, + }); + const passwordBefore = await getUserById(passwordTarget.id); + + const demotionResponse = await testApp.app.inject({ + method: "PUT", + url: `/api/auth/users/${demotionTarget.id}`, + headers: { authorization: `Bearer ${fullPermissionManagerToken}` }, + payload: { role: subordinateRole }, + }); + const passwordResponse = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${passwordTarget.id}/reset-password`, + headers: { authorization: `Bearer ${fullPermissionManagerToken}` }, + payload: { newPassword: "UnauthorizedReset1" }, + }); + const deleteResponse = await testApp.app.inject({ + method: "DELETE", + url: `/api/auth/users/${deleteTarget.id}`, + headers: { authorization: `Bearer ${fullPermissionManagerToken}` }, + }); + const mfaResponse = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${mfaTarget.id}/mfa/reset`, + headers: { authorization: `Bearer ${fullPermissionManagerToken}` }, + }); + + for (const response of [demotionResponse, passwordResponse, deleteResponse, mfaResponse]) { + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + } + + const [demotionAfter, passwordAfter, deleteAfter, mfaAfter] = await Promise.all([ + getUserById(demotionTarget.id), + getUserById(passwordTarget.id), + getUserById(deleteTarget.id), + getUserById(mfaTarget.id), + ]); + expect.soft(demotionAfter?.role).toBe("admin"); + expect.soft(passwordAfter?.passwordHash).toBe(passwordBefore?.passwordHash); + expect.soft(passwordAfter?.mustChangePassword).toBe(false); + expect.soft(deleteAfter?.role).toBe("admin"); + expect(mfaAfter).toMatchObject({ + role: "admin", + totpEnabled: true, + totpSecret: "target-authority-test-secret", + recoveryCodesHash: "target-authority-test-recovery-codes", + }); + }); + + it("denies a custom manager demoting an admin and preserves the admin role", async () => { + const target = await createTargetUser("deny-demote-admin", "admin"); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/auth/users/${target.id}`, + headers: { authorization: `Bearer ${managerToken}` }, + payload: { role: subordinateRole }, + }); + const targetAfter = await getUserById(target.id); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect(targetAfter?.role).toBe("admin"); + }); + + it("denies a custom manager resetting an admin password and preserves credentials", async () => { + const target = await createTargetUser("deny-reset-admin", "admin"); + const targetBefore = await getUserById(target.id); + + const response = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/reset-password`, + headers: { authorization: `Bearer ${managerToken}` }, + payload: { newPassword: "ReplacementPass1" }, + }); + const targetAfter = await getUserById(target.id); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect(targetAfter?.passwordHash).toBe(targetBefore?.passwordHash); + expect(targetAfter?.mustChangePassword).toBe(false); + }); + + it("denies a custom manager deleting an admin and preserves the account", async () => { + const target = await createTargetUser("deny-delete-admin", "admin"); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/auth/users/${target.id}`, + headers: { authorization: `Bearer ${managerToken}` }, + }); + const targetAfter = await getUserById(target.id); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect(targetAfter).toBeDefined(); + expect(targetAfter?.role).toBe("admin"); + }); + + it("denies a custom manager resetting admin MFA and preserves MFA state", async () => { + const target = await createTargetUser("deny-mfa-admin", "admin", { mfaEnabled: true }); + + const response = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/mfa/reset`, + headers: { authorization: `Bearer ${managerToken}` }, + }); + const targetAfter = await getUserById(target.id); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect(targetAfter).toMatchObject({ + role: "admin", + totpEnabled: true, + totpSecret: "target-authority-test-secret", + recoveryCodesHash: "target-authority-test-recovery-codes", + }); + }); + + it("protects a disabled admin from a custom manager while allowing built-in admin recovery", async () => { + const target = await createTargetUser("disabled-admin-recovery", "admin"); + await db + .update(schema.users) + .set({ role: "disabled:admin" }) + .where(eq(schema.users.id, target.id)); + const targetBefore = await getUserById(target.id); + + const deniedResponse = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/reset-password`, + headers: { authorization: `Bearer ${managerToken}` }, + payload: { newPassword: "UnauthorizedReset1" }, + }); + const targetAfterDenial = await getUserById(target.id); + + expect.soft(deniedResponse.statusCode).toBe(403); + expect.soft(JSON.parse(deniedResponse.body).code).toBe("ESCALATION_DENIED"); + expect.soft(targetAfterDenial?.role).toBe("disabled:admin"); + expect.soft(targetAfterDenial?.passwordHash).toBe(targetBefore?.passwordHash); + expect.soft(targetAfterDenial?.mustChangePassword).toBe(false); + + const recoveryResponse = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/reset-password`, + headers: { authorization: `Bearer ${adminToken}` }, + payload: { newPassword: "AdminRecovery1" }, + }); + const targetAfterRecovery = await getUserById(target.id); + + expect(recoveryResponse.statusCode).toBe(200); + expect(targetAfterRecovery?.role).toBe("disabled:admin"); + expect(targetAfterRecovery?.passwordHash).not.toBe(targetBefore?.passwordHash); + expect(targetAfterRecovery?.mustChangePassword).toBe(true); + }); + + it("allows a custom manager to change a subordinate custom-role user", async () => { + const target = await createTargetUser("allow-demote-sub", subordinateRole); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/auth/users/${target.id}`, + headers: { authorization: `Bearer ${managerToken}` }, + payload: { role: alternateSubordinateRole }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter?.role).toBe(alternateSubordinateRole); + }); + + it("allows a custom manager to reset a subordinate custom-role user password", async () => { + const target = await createTargetUser("allow-reset-sub", subordinateRole); + const targetBefore = await getUserById(target.id); + + const response = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/reset-password`, + headers: { authorization: `Bearer ${managerToken}` }, + payload: { newPassword: "ReplacementPass1" }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter?.passwordHash).not.toBe(targetBefore?.passwordHash); + expect(targetAfter?.mustChangePassword).toBe(true); + }); + + it("allows a custom manager to delete a subordinate custom-role user", async () => { + const target = await createTargetUser("allow-delete-sub", subordinateRole); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/auth/users/${target.id}`, + headers: { authorization: `Bearer ${managerToken}` }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter).toBeUndefined(); + }); + + it("allows a custom manager to reset subordinate custom-role user MFA", async () => { + const target = await createTargetUser("allow-mfa-sub", subordinateRole, { mfaEnabled: true }); + + const response = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/mfa/reset`, + headers: { authorization: `Bearer ${managerToken}` }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter).toMatchObject({ + totpEnabled: false, + totpSecret: null, + recoveryCodesHash: null, + }); + }); + + it("allows an admin to demote an equal admin", async () => { + const target = await createTargetUser("allow-demote-peer", "admin"); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/auth/users/${target.id}`, + headers: { authorization: `Bearer ${adminToken}` }, + payload: { role: subordinateRole }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter?.role).toBe(subordinateRole); + }); + + it("allows an admin to reset an equal admin password", async () => { + const target = await createTargetUser("allow-reset-peer", "admin"); + const targetBefore = await getUserById(target.id); + + const response = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/reset-password`, + headers: { authorization: `Bearer ${adminToken}` }, + payload: { newPassword: "ReplacementPass1" }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter?.passwordHash).not.toBe(targetBefore?.passwordHash); + expect(targetAfter?.mustChangePassword).toBe(true); + }); + + it("allows an admin to delete an equal admin", async () => { + const target = await createTargetUser("allow-delete-peer", "admin"); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/auth/users/${target.id}`, + headers: { authorization: `Bearer ${adminToken}` }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter).toBeUndefined(); + }); + + it("allows an admin to reset equal admin MFA", async () => { + const target = await createTargetUser("allow-mfa-peer", "admin", { mfaEnabled: true }); + + const response = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${target.id}/mfa/reset`, + headers: { authorization: `Bearer ${adminToken}` }, + }); + const targetAfter = await getUserById(target.id); + + expect(response.statusCode).toBe(200); + expect(targetAfter).toMatchObject({ + totpEnabled: false, + totpSecret: null, + recoveryCodesHash: null, + }); + }); +}); diff --git a/tests/integration/platform/gdpr-lifecycle.test.ts b/tests/integration/platform/gdpr-lifecycle.test.ts index 89c11c8d..3e078f0f 100644 --- a/tests/integration/platform/gdpr-lifecycle.test.ts +++ b/tests/integration/platform/gdpr-lifecycle.test.ts @@ -1,5 +1,6 @@ +import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { db, schema } from "../../../apps/api/src/db/index.js"; import { buildTestApp, loginAsAdmin, type TestApp } from "../test-server.js"; @@ -262,3 +263,198 @@ describe("GDPR edge cases", () => { expect(res.statusCode).toBe(404); }); }); + +describe("GDPR purge role hierarchy", () => { + let licensedApp: TestApp; + let licensedAdminToken: string; + let complianceManagerToken: string; + let complianceRoleId: string; + let complianceManagerId: string; + let targetSequence = 0; + + beforeAll(async () => { + vi.resetModules(); + const { mockEnterpriseFeatures } = await import("../../helpers/enterprise-mock.js"); + mockEnterpriseFeatures(["gdpr_lifecycle"]); + const { buildTestApp, loginAsAdmin } = await import("../test-server.js"); + licensedApp = await buildTestApp(); + licensedAdminToken = await loginAsAdmin(licensedApp.app); + + const suffix = Date.now().toString(36); + const roleName = `compliance-${suffix}`; + const username = `compliance-manager-${suffix}`; + const roleRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { name: roleName, permissions: ["compliance:manage"] }, + }); + if (roleRes.statusCode !== 201) { + throw new Error(`Failed to create compliance manager role: ${roleRes.body}`); + } + complianceRoleId = JSON.parse(roleRes.body).id as string; + + const registerRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { username, password: "TestPass1", role: roleName }, + }); + if (registerRes.statusCode !== 201) { + throw new Error(`Failed to create compliance manager user: ${registerRes.body}`); + } + complianceManagerId = JSON.parse(registerRes.body).id as string; + await db + .update(schema.users) + .set({ mustChangePassword: false }) + .where(eq(schema.users.id, complianceManagerId)); + + const loginRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username, password: "TestPass1" }, + }); + complianceManagerToken = JSON.parse(loginRes.body).token as string; + }, 30_000); + + afterAll(async () => { + if (complianceManagerId) { + await db.delete(schema.users).where(eq(schema.users.id, complianceManagerId)); + } + if (complianceRoleId) { + await db.delete(schema.roles).where(eq(schema.roles.id, complianceRoleId)); + } + await licensedApp.cleanup(); + vi.restoreAllMocks(); + }, 10_000); + + async function createTarget(role: string, teamId?: string): Promise { + targetSequence += 1; + const username = `gdpr-target-${role}-${Date.now().toString(36)}-${targetSequence}`; + const registerRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { username, password: "TargetPass1", role }, + }); + if (registerRes.statusCode !== 201) { + throw new Error(`Failed to create GDPR target user: ${registerRes.body}`); + } + const id = JSON.parse(registerRes.body).id as string; + await db + .update(schema.users) + .set({ + mustChangePassword: false, + ...(teamId ? { team: teamId } : {}), + }) + .where(eq(schema.users.id, id)); + return id; + } + + it("denies direct purge of a disabled administrator and preserves the account", async () => { + const targetId = await createTarget("admin"); + await db + .update(schema.users) + .set({ role: "disabled:admin" }) + .where(eq(schema.users.id, targetId)); + + try { + const res = await licensedApp.app.inject({ + method: "DELETE", + url: `/api/v1/enterprise/users/${targetId}/purge`, + headers: { authorization: `Bearer ${complianceManagerToken}` }, + payload: { confirm: true }, + }); + const body = JSON.parse(res.body); + const [remainingTarget] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, targetId)); + + expect.soft(res.statusCode).toBe(403); + expect.soft(body.code).toBe("ESCALATION_DENIED"); + expect(remainingTarget?.role).toBe("disabled:admin"); + } finally { + await db.delete(schema.users).where(eq(schema.users.id, targetId)); + } + }); + + it("preflights a mixed team and preserves subordinate members when a disabled admin is denied", async () => { + const teamId = randomUUID(); + await db.insert(schema.teams).values({ + id: teamId, + name: `GDPR hierarchy ${Date.now().toString(36)}`, + }); + const subordinateId = await createTarget("user", teamId); + const disabledAdminId = await createTarget("admin", teamId); + await db + .update(schema.users) + .set({ role: "disabled:admin" }) + .where(eq(schema.users.id, disabledAdminId)); + + try { + const res = await licensedApp.app.inject({ + method: "DELETE", + url: `/api/v1/enterprise/teams/${teamId}/purge`, + headers: { authorization: `Bearer ${complianceManagerToken}` }, + payload: { confirm: true }, + }); + const body = JSON.parse(res.body); + const [remainingSubordinate] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, subordinateId)); + const [remainingDisabledAdmin] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, disabledAdminId)); + const [remainingTeam] = await db + .select() + .from(schema.teams) + .where(eq(schema.teams.id, teamId)); + + expect.soft(res.statusCode).toBe(403); + expect.soft(body.code).toBe("ESCALATION_DENIED"); + expect.soft(remainingSubordinate?.role).toBe("user"); + expect.soft(remainingDisabledAdmin?.role).toBe("disabled:admin"); + expect(remainingTeam?.id).toBe(teamId); + } finally { + await db.delete(schema.users).where(eq(schema.users.id, subordinateId)); + await db.delete(schema.users).where(eq(schema.users.id, disabledAdminId)); + await db.delete(schema.teams).where(eq(schema.teams.id, teamId)); + } + }); + + it("allows the full built-in admin to purge a subordinate team", async () => { + const teamId = randomUUID(); + await db.insert(schema.teams).values({ + id: teamId, + name: `GDPR subordinate ${Date.now().toString(36)}`, + }); + const targetId = await createTarget("user", teamId); + + try { + const res = await licensedApp.app.inject({ + method: "DELETE", + url: `/api/v1/enterprise/teams/${teamId}/purge`, + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { confirm: true }, + }); + const [remainingTarget] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, targetId)); + const [remainingTeam] = await db + .select() + .from(schema.teams) + .where(eq(schema.teams.id, teamId)); + + expect(res.statusCode).toBe(200); + expect(remainingTarget).toBeUndefined(); + expect(remainingTeam).toBeUndefined(); + } finally { + await db.delete(schema.users).where(eq(schema.users.id, targetId)); + await db.delete(schema.teams).where(eq(schema.teams.id, teamId)); + } + }); +}); diff --git a/tests/integration/platform/role-authority.test.ts b/tests/integration/platform/role-authority.test.ts new file mode 100644 index 00000000..461d8e33 --- /dev/null +++ b/tests/integration/platform/role-authority.test.ts @@ -0,0 +1,542 @@ +import { eq, sql } from "drizzle-orm"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { db, schema } from "../../../apps/api/src/db/index.js"; +import { canManageTargetRole, hasToolAccess } from "../../../apps/api/src/permissions.js"; +import { buildTestApp, loginAsAdmin, type TestApp } from "../test-server.js"; + +let testApp: TestApp; +let adminToken: string; +let scopeManagerToken: string; +let scopeManagerRole: string; +let sequence = 0; + +const testRun = Date.now().toString(36); + +function uniqueName(prefix: string): string { + sequence += 1; + return `${prefix}-${testRun}-${sequence}`; +} + +async function createRoleAsAdmin( + name: string, + permissions: string[], + toolPermissions?: { mode: "category" | "tool"; allowed: string[] } | null, +): Promise { + const response = await testApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${adminToken}` }, + payload: { name, permissions, toolPermissions }, + }); + + expect(response.statusCode, response.body).toBe(201); + return JSON.parse(response.body).id as string; +} + +async function createUserAndLogin(username: string, role: string): Promise { + const password = "RoleAuthority1!"; + const registerResponse = await testApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${adminToken}` }, + payload: { username, password, role }, + }); + expect(registerResponse.statusCode, registerResponse.body).toBe(201); + + await db + .update(schema.users) + .set({ mustChangePassword: false }) + .where(eq(schema.users.username, username)); + + const loginResponse = await testApp.app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username, password }, + }); + expect(loginResponse.statusCode, loginResponse.body).toBe(200); + return JSON.parse(loginResponse.body).token as string; +} + +beforeAll(async () => { + testApp = await buildTestApp(); + adminToken = await loginAsAdmin(testApp.app); + + scopeManagerRole = uniqueName("scope-manager"); + await createRoleAsAdmin(scopeManagerRole, ["security:manage", "tools:use"], { + mode: "category", + allowed: ["image"], + }); + scopeManagerToken = await createUserAndLogin(uniqueName("scope-manager-user"), scopeManagerRole); +}, 30_000); + +afterAll(async () => { + await testApp.cleanup(); +}, 10_000); + +describe("role authority containment", () => { + it.each(["disabled:", "disabled:disabled", "disabled:disabled:", "disabled:disabled:admin"])( + "normalizes malformed disabled role %s to the conservative admin boundary", + async (role) => { + const admin = { id: "admin", username: "admin", role: "admin" }; + const customManager = { + id: "scope-manager", + username: "scope-manager", + role: scopeManagerRole, + }; + + expect.soft(await canManageTargetRole(admin, role)).toBe(true); + expect(await canManageTargetRole(customManager, role)).toBe(false); + }, + ); + + it("rejects updating a role whose existing permissions exceed the actor's authority", async () => { + const targetName = uniqueName("broader-update"); + const targetId = await createRoleAsAdmin(targetName, ["settings:write"]); + const [before] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { description: "unauthorized change" }, + }); + + expect(response.statusCode).toBe(403); + expect(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + const [after] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + expect(after).toMatchObject({ + description: before.description, + permissions: before.permissions, + toolPermissions: before.toolPermissions, + }); + }); + + it("rejects adding an ordinary permission the actor does not hold", async () => { + const targetName = uniqueName("broader-permission"); + const targetId = await createRoleAsAdmin(targetName, ["security:manage"]); + const [before] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { permissions: ["security:manage", "settings:write"] }, + }); + const [after] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect(after.permissions).toEqual(before.permissions); + }); + + it("rejects deleting a role whose existing permissions exceed the actor's authority", async () => { + const targetName = uniqueName("broader-delete"); + const targetId = await createRoleAsAdmin(targetName, ["settings:write"]); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + }); + + expect(response.statusCode).toBe(403); + expect(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + const [persisted] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + expect(persisted).toMatchObject({ id: targetId, name: targetName }); + }); + + it("rejects creating a role with tool access outside the actor's tool scope", async () => { + const targetName = uniqueName("broader-tool-create"); + + const response = await testApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { + name: targetName, + permissions: ["tools:use"], + toolPermissions: { mode: "category", allowed: ["video"] }, + }, + }); + + expect(response.statusCode).toBe(403); + expect(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + const [persisted] = await db + .select() + .from(schema.roles) + .where(eq(schema.roles.name, targetName)); + expect(persisted).toBeUndefined(); + }); + + it("rejects widening an existing role beyond the actor's tool scope", async () => { + const targetName = uniqueName("broader-tool-update"); + const targetId = await createRoleAsAdmin(targetName, ["tools:use"], { + mode: "category", + allowed: ["image"], + }); + const [before] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { toolPermissions: { mode: "category", allowed: ["image", "video"] } }, + }); + + expect(response.statusCode).toBe(403); + expect(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + const [after] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + expect(after.toolPermissions).toEqual(before.toolPermissions); + }); + + it("fails closed for malformed persisted custom-role tool permissions", async () => { + const targetName = uniqueName("malformed-tools"); + const targetId = await createRoleAsAdmin(targetName, ["tools:use"]); + await db + .update(schema.roles) + .set({ + toolPermissions: sql`${JSON.stringify({ mode: "unexpected", allowed: [] })}::jsonb`, + }) + .where(eq(schema.roles.id, targetId)); + + const admin = { id: "admin", username: "admin", role: "admin" }; + expect.soft(await canManageTargetRole(admin, targetName)).toBe(false); + expect(await hasToolAccess(targetName, "resize")).toBe(false); + }); + + it("rejects null unrestricted tool scopes on create and update", async () => { + const createdName = uniqueName("null-tool-create"); + const createResponse = await testApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { + name: createdName, + permissions: ["tools:use"], + toolPermissions: null, + }, + }); + + expect.soft(createResponse.statusCode).toBe(403); + expect.soft(JSON.parse(createResponse.body).code).toBe("ESCALATION_DENIED"); + const [unexpectedCreate] = await db + .select() + .from(schema.roles) + .where(eq(schema.roles.name, createdName)); + expect.soft(unexpectedCreate).toBeUndefined(); + + const updatedName = uniqueName("null-tool-update"); + const updatedId = await createRoleAsAdmin(updatedName, ["tools:use"], { + mode: "category", + allowed: ["image"], + }); + const [before] = await db.select().from(schema.roles).where(eq(schema.roles.id, updatedId)); + const updateResponse = await testApp.app.inject({ + method: "PUT", + url: `/api/v1/roles/${updatedId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { toolPermissions: null }, + }); + const [after] = await db.select().from(schema.roles).where(eq(schema.roles.id, updatedId)); + + expect.soft(updateResponse.statusCode).toBe(403); + expect.soft(JSON.parse(updateResponse.body).code).toBe("ESCALATION_DENIED"); + expect(after.toolPermissions).toEqual(before.toolPermissions); + }); + + it("matches tool-scope containment to graceful degradation without the enterprise feature", async () => { + const actorRole = uniqueName("deg-manager"); + await createRoleAsAdmin(actorRole, ["security:manage", "tools:use"], { + mode: "tool", + allowed: ["resize"], + }); + const actorToken = await createUserAndLogin(uniqueName("deg-user"), actorRole); + const targetName = uniqueName("deg-target"); + + const response = await testApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${actorToken}` }, + payload: { + name: targetName, + permissions: ["tools:use"], + toolPermissions: null, + }, + }); + + expect(response.statusCode, response.body).toBe(201); + }); + + it("allows creating and updating a contained role and lets a full admin delete it", async () => { + const targetName = uniqueName("contained-role"); + const createResponse = await testApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { + name: targetName, + permissions: ["tools:use"], + toolPermissions: { mode: "category", allowed: ["image"] }, + }, + }); + expect(createResponse.statusCode, createResponse.body).toBe(201); + const targetId = JSON.parse(createResponse.body).id as string; + + const updateResponse = await testApp.app.inject({ + method: "PUT", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + payload: { description: "contained update" }, + }); + expect(updateResponse.statusCode, updateResponse.body).toBe(200); + + const deleteResponse = await testApp.app.inject({ + method: "DELETE", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${adminToken}` }, + }); + expect(deleteResponse.statusCode, deleteResponse.body).toBe(200); + + const [persisted] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + expect(persisted).toBeUndefined(); + }); + + it("renames active and disabled custom-role members atomically", async () => { + const originalName = uniqueName("rename-role"); + const renamedName = uniqueName("renamed-role"); + const roleId = await createRoleAsAdmin(originalName, ["security:manage"]); + const activeUsername = uniqueName("rename-active"); + const disabledUsername = uniqueName("rename-disabled"); + const nestedUsername = uniqueName("rename-nested"); + const lookalikeUsername = uniqueName("rename-lookalike"); + await createUserAndLogin(activeUsername, originalName); + await createUserAndLogin(disabledUsername, originalName); + await createUserAndLogin(nestedUsername, originalName); + await createUserAndLogin(lookalikeUsername, originalName); + await db + .update(schema.users) + .set({ role: `disabled:${originalName}` }) + .where(eq(schema.users.username, disabledUsername)); + await db + .update(schema.users) + .set({ role: `disabled:disabled:disabled:${originalName}` }) + .where(eq(schema.users.username, nestedUsername)); + const lookalikeRole = `disabled:disabled:${originalName}-suffix`; + await db + .update(schema.users) + .set({ role: lookalikeRole }) + .where(eq(schema.users.username, lookalikeUsername)); + + const response = await testApp.app.inject({ + method: "PUT", + url: `/api/v1/roles/${roleId}`, + headers: { authorization: `Bearer ${adminToken}` }, + payload: { name: renamedName }, + }); + const [renamedRole] = await db.select().from(schema.roles).where(eq(schema.roles.id, roleId)); + const [activeMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, activeUsername)); + const [disabledMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, disabledUsername)); + const [nestedMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, nestedUsername)); + const [lookalikeMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, lookalikeUsername)); + + expect.soft(response.statusCode, response.body).toBe(200); + expect.soft(renamedRole?.name).toBe(renamedName); + expect.soft(activeMember?.role).toBe(renamedName); + expect.soft(disabledMember?.role).toBe(`disabled:${renamedName}`); + expect.soft(nestedMember?.role).toBe(`disabled:${renamedName}`); + expect(lookalikeMember?.role).toBe(lookalikeRole); + }); + + it("preserves member activation state when a full admin deletes a custom role", async () => { + const targetName = uniqueName("delete-role"); + const targetId = await createRoleAsAdmin(targetName, ["security:manage"]); + const activeUsername = uniqueName("delete-active"); + const disabledUsername = uniqueName("delete-disabled"); + const nestedUsername = uniqueName("delete-nested"); + const lookalikeUsername = uniqueName("delete-lookalike"); + await createUserAndLogin(activeUsername, targetName); + await createUserAndLogin(disabledUsername, targetName); + await createUserAndLogin(nestedUsername, targetName); + await createUserAndLogin(lookalikeUsername, targetName); + await db + .update(schema.users) + .set({ role: `disabled:${targetName}` }) + .where(eq(schema.users.username, disabledUsername)); + await db + .update(schema.users) + .set({ role: `disabled:disabled:disabled:${targetName}` }) + .where(eq(schema.users.username, nestedUsername)); + const lookalikeRole = `disabled:disabled:${targetName}-suffix`; + await db + .update(schema.users) + .set({ role: lookalikeRole }) + .where(eq(schema.users.username, lookalikeUsername)); + const [disabledBefore] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, disabledUsername)); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${adminToken}` }, + }); + const [deletedRole] = await db.select().from(schema.roles).where(eq(schema.roles.id, targetId)); + const [activeMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, activeUsername)); + const [disabledMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, disabledUsername)); + const [nestedMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, nestedUsername)); + const [lookalikeMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, lookalikeUsername)); + + expect.soft(response.statusCode, response.body).toBe(200); + expect.soft(deletedRole).toBeUndefined(); + expect.soft(activeMember?.role).toBe("user"); + expect.soft(disabledMember?.role).toBe("disabled:user"); + expect.soft(nestedMember?.role).toBe("disabled:user"); + expect.soft(lookalikeMember?.role).toBe(lookalikeRole); + + const manageResponse = await testApp.app.inject({ + method: "PUT", + url: `/api/auth/users/${disabledBefore.id}`, + headers: { authorization: `Bearer ${adminToken}` }, + payload: { role: "user" }, + }); + const [reactivatedMember] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, disabledBefore.id)); + + expect.soft(manageResponse.statusCode, manageResponse.body).toBe(200); + expect(reactivatedMember?.role).toBe("user"); + }); + + it.each(["active", "disabled"])( + "rejects deleting a contained role with an %s member when the user fallback exceeds actor authority", + async (memberState) => { + const deletionManagerRole = uniqueName("delete-manager"); + await createRoleAsAdmin(deletionManagerRole, ["security:manage", "users:manage"]); + const deletionManagerToken = await createUserAndLogin( + uniqueName("delete-mgr-user"), + deletionManagerRole, + ); + + const targetName = uniqueName("occupied-role"); + const targetId = await createRoleAsAdmin(targetName, ["security:manage"]); + const memberName = uniqueName("occupied-member"); + await createUserAndLogin(memberName, targetName); + if (memberState === "disabled") { + await db + .update(schema.users) + .set({ role: `disabled:${targetName}` }) + .where(eq(schema.users.username, memberName)); + } + const [memberBefore] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, memberName)); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${deletionManagerToken}` }, + }); + const [persistedRole] = await db + .select() + .from(schema.roles) + .where(eq(schema.roles.id, targetId)); + const [memberAfter] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, memberBefore.id)); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect.soft(persistedRole).toMatchObject({ id: targetId, name: targetName }); + expect(memberAfter?.role).toBe(memberBefore.role); + }, + ); + + it("rejects deleting an empty contained role when the fallback exceeds actor authority", async () => { + const targetName = uniqueName("empty-role"); + const targetId = await createRoleAsAdmin(targetName, ["security:manage"]); + + const response = await testApp.app.inject({ + method: "DELETE", + url: `/api/v1/roles/${targetId}`, + headers: { authorization: `Bearer ${scopeManagerToken}` }, + }); + const [persistedRole] = await db + .select() + .from(schema.roles) + .where(eq(schema.roles.id, targetId)); + + expect.soft(response.statusCode).toBe(403); + expect.soft(JSON.parse(response.body).code).toBe("ESCALATION_DENIED"); + expect(persistedRole).toMatchObject({ id: targetId, name: targetName }); + }); +}); + +describe("API-key-scoped role authority", () => { + it("does not let a users:manage-only admin API key reset a peer administrator", async () => { + const targetUsername = uniqueName("peer-admin"); + const targetPassword = "PeerAdmin1!"; + const registerResponse = await testApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${adminToken}` }, + payload: { username: targetUsername, password: targetPassword, role: "admin" }, + }); + expect(registerResponse.statusCode, registerResponse.body).toBe(201); + const targetId = JSON.parse(registerResponse.body).id as string; + await db + .update(schema.users) + .set({ mustChangePassword: false }) + .where(eq(schema.users.id, targetId)); + const [before] = await db.select().from(schema.users).where(eq(schema.users.id, targetId)); + + const keyResponse = await testApp.app.inject({ + method: "POST", + url: "/api/v1/api-keys", + headers: { authorization: `Bearer ${adminToken}` }, + payload: { name: uniqueName("users-manage-key"), permissions: ["users:manage"] }, + }); + expect(keyResponse.statusCode, keyResponse.body).toBe(201); + const apiKey = JSON.parse(keyResponse.body).key as string; + + const resetResponse = await testApp.app.inject({ + method: "POST", + url: `/api/auth/users/${targetId}/reset-password`, + headers: { authorization: `Bearer ${apiKey}` }, + payload: { newPassword: "UnauthorizedReset1!" }, + }); + + expect(resetResponse.statusCode).toBe(403); + expect(JSON.parse(resetResponse.body).code).toBe("ESCALATION_DENIED"); + const [after] = await db.select().from(schema.users).where(eq(schema.users.id, targetId)); + expect(after.passwordHash).toBe(before.passwordHash); + expect(after.mustChangePassword).toBe(false); + }); +}); diff --git a/tests/integration/platform/scim.test.ts b/tests/integration/platform/scim.test.ts index e7e3f591..f011f9c0 100644 --- a/tests/integration/platform/scim.test.ts +++ b/tests/integration/platform/scim.test.ts @@ -1,10 +1,30 @@ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { eq } from "drizzle-orm"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { db, schema } from "../../../apps/api/src/db/index.js"; -import { hashPassword } from "../../../apps/api/src/plugins/auth.js"; +import { hashPassword, verifyPassword } from "../../../apps/api/src/plugins/auth.js"; import { buildTestApp, type TestApp } from "../test-server.js"; let testApp: TestApp; -const SCIM_TOKEN = "test-scim-token-abc123"; +const SCIM_TOKEN = `so_scim_v2_${"a".repeat(64)}`; +const ADMIN_PERMISSIONS = [ + "tools:use", + "files:own", + "files:all", + "apikeys:own", + "apikeys:all", + "pipelines:own", + "pipelines:all", + "settings:read", + "settings:write", + "users:manage", + "teams:manage", + "features:manage", + "system:health", + "audit:read", + "compliance:manage", + "webhooks:manage", + "security:manage", +]; beforeAll(async () => { testApp = await buildTestApp(); @@ -175,6 +195,38 @@ describe("SCIM 2.0 provisioning", () => { }); expect(res.statusCode).toBe(401); }); + + it("rejects a correctly hashed legacy unversioned token", async () => { + const legacyToken = "b".repeat(64); + const legacyHash = await hashPassword(legacyToken); + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: legacyHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: legacyHash }, + }); + + try { + const res = await testApp.app.inject({ + method: "GET", + url: "/api/v1/scim/v2/Users", + headers: { authorization: `Bearer ${legacyToken}` }, + }); + + expect(res.statusCode).toBe(401); + expect(JSON.parse(res.body)).toMatchObject({ + status: 401, + detail: "Invalid token", + }); + } finally { + const currentHash = await hashPassword(SCIM_TOKEN); + await db + .update(schema.settings) + .set({ value: currentHash }) + .where(eq(schema.settings.key, "scim_token_hash")); + } + }); }); // ── Enterprise gate ──────────────────────────────────────────── @@ -322,3 +374,398 @@ describe("SCIM 2.0 provisioning", () => { }); }); }); + +describe("SCIM global token administration", () => { + let licensedApp: TestApp; + let licensedAdminToken: string; + let managerToken: string; + let managerRoleId: string; + let managerUserId: string; + let scopedAdminKey: string; + + beforeAll(async () => { + vi.resetModules(); + const { mockEnterpriseFeatures } = await import("../../helpers/enterprise-mock.js"); + mockEnterpriseFeatures(["scim"]); + const { buildTestApp, loginAsAdmin } = await import("../test-server.js"); + licensedApp = await buildTestApp(); + licensedAdminToken = await loginAsAdmin(licensedApp.app); + + const suffix = Date.now().toString(36); + const roleName = `scim-manager-${suffix}`; + const username = `scim-manager-user-${suffix}`; + const roleRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/roles", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { name: roleName, permissions: ADMIN_PERMISSIONS }, + }); + if (roleRes.statusCode !== 201) { + throw new Error(`Failed to create SCIM manager role: ${roleRes.body}`); + } + managerRoleId = JSON.parse(roleRes.body).id as string; + + const registerRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/register", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { username, password: "TestPass1", role: roleName }, + }); + if (registerRes.statusCode !== 201) { + throw new Error(`Failed to create SCIM manager user: ${registerRes.body}`); + } + managerUserId = JSON.parse(registerRes.body).id as string; + await db + .update(schema.users) + .set({ mustChangePassword: false }) + .where(eq(schema.users.id, managerUserId)); + + const loginRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { username, password: "TestPass1" }, + }); + managerToken = JSON.parse(loginRes.body).token as string; + + const apiKeyRes = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/api-keys", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + payload: { + name: `scim-scoped-admin-${suffix}`, + permissions: ["users:manage", "apikeys:own"], + }, + }); + if (apiKeyRes.statusCode !== 201) { + throw new Error(`Failed to create scoped admin API key: ${apiKeyRes.body}`); + } + scopedAdminKey = JSON.parse(apiKeyRes.body).key as string; + }, 30_000); + + afterAll(async () => { + await db.delete(schema.settings).where(eq(schema.settings.key, "scim_token_hash")); + if (managerUserId) { + await db.delete(schema.users).where(eq(schema.users.id, managerUserId)); + } + if (managerRoleId) { + await db.delete(schema.roles).where(eq(schema.roles.id, managerRoleId)); + } + await licensedApp.cleanup(); + vi.restoreAllMocks(); + }, 10_000); + + it("denies token issuance to a custom role even when it has every admin permission", async () => { + const originalHash = "scim-issuance-authorization-sentinel"; + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: originalHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: originalHash }, + }); + + const res = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/enterprise/scim/token", + headers: { authorization: `Bearer ${managerToken}` }, + }); + const body = JSON.parse(res.body); + const [storedToken] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, "scim_token_hash")); + + expect.soft(res.statusCode).toBe(403); + expect.soft(body.code).toBe("ESCALATION_DENIED"); + expect(storedToken?.value).toBe(originalHash); + }); + + it("denies token revocation to a custom role even when it has every admin permission", async () => { + const originalHash = "scim-revocation-authorization-sentinel"; + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: originalHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: originalHash }, + }); + + const res = await licensedApp.app.inject({ + method: "DELETE", + url: "/api/v1/enterprise/scim/token", + headers: { authorization: `Bearer ${managerToken}` }, + }); + const body = res.body ? JSON.parse(res.body) : {}; + const [storedToken] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, "scim_token_hash")); + + expect.soft(res.statusCode).toBe(403); + expect.soft(body.code).toBe("ESCALATION_DENIED"); + expect(storedToken?.value).toBe(originalHash); + }); + + it.each([ + { method: "POST" as const, operation: "issuance" }, + { method: "DELETE" as const, operation: "revocation" }, + ])("denies token $operation through a scoped built-in admin API key", async ({ method }) => { + const originalHash = `scim-scoped-key-${method.toLowerCase()}-sentinel`; + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: originalHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: originalHash }, + }); + + const res = await licensedApp.app.inject({ + method, + url: "/api/v1/enterprise/scim/token", + headers: { authorization: `Bearer ${scopedAdminKey}` }, + }); + const [storedToken] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, "scim_token_hash")); + + expect.soft(res.statusCode).toBe(403); + expect.soft(JSON.parse(res.body).code).toBe("ESCALATION_DENIED"); + expect(storedToken?.value).toBe(originalHash); + }); + + it("issues a versioned token that authenticates an end-to-end SCIM request", async () => { + await db.delete(schema.settings).where(eq(schema.settings.key, "scim_token_hash")); + + const res = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/enterprise/scim/token", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + }); + const body = JSON.parse(res.body) as { token: string }; + const [storedToken] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, "scim_token_hash")); + + expect(res.statusCode).toBe(201); + expect(body.token).toMatch(/^so_scim_v2_[0-9a-f]{64}$/); + if (!storedToken) throw new Error("SCIM token hash was not persisted"); + expect(await verifyPassword(body.token, storedToken.value)).toBe(true); + + const listRes = await licensedApp.app.inject({ + method: "GET", + url: "/api/v1/scim/v2/Users", + headers: { authorization: `Bearer ${body.token}` }, + }); + expect(listRes.statusCode, listRes.body).toBe(200); + expect(JSON.parse(listRes.body).Resources).toBeInstanceOf(Array); + }); + + it("allows the full built-in admin to revoke a token", async () => { + const tokenHash = await hashPassword(SCIM_TOKEN); + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: tokenHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: tokenHash }, + }); + + const res = await licensedApp.app.inject({ + method: "DELETE", + url: "/api/v1/enterprise/scim/token", + headers: { authorization: `Bearer ${licensedAdminToken}` }, + }); + const [storedToken] = await db + .select() + .from(schema.settings) + .where(eq(schema.settings.key, "scim_token_hash")); + + expect(res.statusCode).toBe(204); + expect(storedToken).toBeUndefined(); + }); + + it("keeps repeated user deprovisioning idempotent and recoverable", async () => { + const tokenHash = await hashPassword(SCIM_TOKEN); + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: tokenHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: tokenHash }, + }); + + const username = `scim-repeat-delete-${Date.now().toString(36)}`; + const createResponse = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/scim/v2/Users", + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + payload: { userName: username, active: true }, + }); + expect(createResponse.statusCode, createResponse.body).toBe(201); + const userId = JSON.parse(createResponse.body).id as string; + + const firstDelete = await licensedApp.app.inject({ + method: "DELETE", + url: `/api/v1/scim/v2/Users/${userId}`, + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + }); + const [afterFirstDelete] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, userId)); + + const secondDelete = await licensedApp.app.inject({ + method: "DELETE", + url: `/api/v1/scim/v2/Users/${userId}`, + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + }); + const [afterSecondDelete] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, userId)); + + expect.soft(firstDelete.statusCode).toBe(204); + expect.soft(secondDelete.statusCode).toBe(204); + expect.soft(afterFirstDelete?.role).toBe("disabled:user"); + expect.soft(afterSecondDelete?.role).toBe("disabled:user"); + + const reactivateResponse = await licensedApp.app.inject({ + method: "PUT", + url: `/api/v1/scim/v2/Users/${userId}`, + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + payload: { userName: username, active: true }, + }); + const [reactivated] = await db.select().from(schema.users).where(eq(schema.users.id, userId)); + + expect.soft(reactivateResponse.statusCode, reactivateResponse.body).toBe(200); + expect.soft(JSON.parse(reactivateResponse.body).active).toBe(true); + expect(reactivated?.role).toBe("user"); + }); + + it("canonicalizes persisted nested disabled markers during deactivation and activation", async () => { + const tokenHash = await hashPassword(SCIM_TOKEN); + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: tokenHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: tokenHash }, + }); + + const username = `scim-nested-disabled-${Date.now().toString(36)}`; + const createResponse = await licensedApp.app.inject({ + method: "POST", + url: "/api/v1/scim/v2/Users", + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + payload: { userName: username, active: true }, + }); + expect(createResponse.statusCode, createResponse.body).toBe(201); + const userId = JSON.parse(createResponse.body).id as string; + + await db + .update(schema.users) + .set({ role: "disabled:disabled:disabled:user" }) + .where(eq(schema.users.id, userId)); + const deleteResponse = await licensedApp.app.inject({ + method: "DELETE", + url: `/api/v1/scim/v2/Users/${userId}`, + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + }); + const [afterDelete] = await db.select().from(schema.users).where(eq(schema.users.id, userId)); + + expect.soft(deleteResponse.statusCode).toBe(204); + expect.soft(afterDelete?.role).toBe("disabled:user"); + + await db + .update(schema.users) + .set({ role: "disabled:disabled:disabled:user" }) + .where(eq(schema.users.id, userId)); + const reactivateResponse = await licensedApp.app.inject({ + method: "PUT", + url: `/api/v1/scim/v2/Users/${userId}`, + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + payload: { userName: username, active: true }, + }); + const [afterReactivation] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, userId)); + + expect.soft(reactivateResponse.statusCode, reactivateResponse.body).toBe(200); + expect.soft(JSON.parse(reactivateResponse.body).active).toBe(true); + expect(afterReactivation?.role).toBe("user"); + }); + + it.each([ + { + method: "PUT" as const, + payload: { userName: "admin", active: false }, + }, + { + method: "PATCH" as const, + payload: { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [{ op: "Replace", path: "active", value: false }], + }, + }, + { + method: "PATCH" as const, + payload: { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [{ op: "Replace", value: { active: false } }], + }, + }, + { + method: "DELETE" as const, + payload: undefined, + }, + ])("$method refuses to deactivate the last active administrator", async ({ method, payload }) => { + const [adminBefore] = await db + .select() + .from(schema.users) + .where(eq(schema.users.username, "admin")); + if (!adminBefore) throw new Error("Default administrator is missing"); + + const activeAdmins = (await db.select().from(schema.users)).filter( + (candidate) => candidate.role === "admin", + ); + expect(activeAdmins).toHaveLength(1); + + const tokenHash = await hashPassword(SCIM_TOKEN); + await db + .insert(schema.settings) + .values({ key: "scim_token_hash", value: tokenHash }) + .onConflictDoUpdate({ + target: schema.settings.key, + set: { value: tokenHash }, + }); + + try { + const res = await licensedApp.app.inject({ + method, + url: `/api/v1/scim/v2/Users/${adminBefore.id}`, + headers: { authorization: `Bearer ${SCIM_TOKEN}` }, + ...(payload === undefined ? {} : { payload }), + }); + const [adminAfter] = await db + .select() + .from(schema.users) + .where(eq(schema.users.id, adminBefore.id)); + + expect.soft(res.statusCode).toBe(409); + expect.soft(JSON.parse(res.body)).toMatchObject({ + status: 409, + detail: "Cannot deactivate the last active administrator", + }); + expect.soft(adminAfter?.role).toBe("admin"); + expect(adminAfter?.passwordHash).toBe(adminBefore.passwordHash); + } finally { + await db + .update(schema.users) + .set({ role: "admin", passwordHash: adminBefore.passwordHash }) + .where(eq(schema.users.id, adminBefore.id)); + } + }); +});