From fe4f6aae54ccb74b6f75126d40bb5cd873dee6ae Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 20 Nov 2023 19:49:51 +0200 Subject: [PATCH 1/6] feat(backend): Introduce OrganizationRoleAPI --- .changeset/cold-comics-serve.md | 5 + .../src/api/endpoints/OrganizationRoleApi.ts | 98 +++++++++++++++++++ packages/backend/src/api/endpoints/index.ts | 1 + packages/backend/src/api/factory.ts | 2 + packages/backend/src/api/resources/JSON.ts | 11 +++ .../src/api/resources/OrganizationRole.ts | 25 +++++ packages/backend/src/api/resources/index.ts | 1 + 7 files changed, 143 insertions(+) create mode 100644 .changeset/cold-comics-serve.md create mode 100644 packages/backend/src/api/endpoints/OrganizationRoleApi.ts create mode 100644 packages/backend/src/api/resources/OrganizationRole.ts diff --git a/.changeset/cold-comics-serve.md b/.changeset/cold-comics-serve.md new file mode 100644 index 00000000000..9a81d8d480f --- /dev/null +++ b/.changeset/cold-comics-serve.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Add OrganizationRoleAPI for CRUD operations regarding instance level organization roles. diff --git a/packages/backend/src/api/endpoints/OrganizationRoleApi.ts b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts new file mode 100644 index 00000000000..7686fc14fc3 --- /dev/null +++ b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts @@ -0,0 +1,98 @@ +import { joinPaths } from '../../util/path'; +import type { DeletedObject } from '../resources'; +import type { OrganizationRole } from '../resources/OrganizationRole'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/organizations_roles'; + +type GetOrganizationRoleListParams = { + limit?: number; + offset?: number; +}; + +type CreateParams = { + name: string; + key: string; + description: string; + permissions: string[]; +}; + +type GetOrganizationRoleParams = { roleId: string }; + +type UpdateParams = { + name?: string; + description?: string; + //TODO(@pantelis): Will this override or union ? + permissions?: string[]; +}; + +type RemovePermissionParams = { + permissionId: string; + roleId: string; +}; + +type AssignPermissionParams = RemovePermissionParams; + +export class OrganizationRoleAPI extends AbstractAPI { + public async getOrganizationRoleList(params?: GetOrganizationRoleListParams) { + return this.request({ + method: 'GET', + path: basePath, + queryParams: params, + }); + } + + public async createOrganizationRole(params: CreateParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams: params, + }); + } + + public async getOrganizationRole(params: GetOrganizationRoleParams) { + this.requireId(params.roleId); + + return this.request({ + method: 'GET', + path: joinPaths(basePath, params.roleId), + }); + } + + public async updateOrganizationRole(roleId: string, params: UpdateParams) { + this.requireId(roleId); + return this.request({ + method: 'PATCH', + path: joinPaths(basePath, roleId), + bodyParams: params, + }); + } + + public async deleteOrganizationRole(roleId: string) { + this.requireId(roleId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, roleId), + }); + } + + public async assignPermissionToRole(params: AssignPermissionParams) { + const { roleId, permissionId } = params; + this.requireId(roleId); + this.requireId(permissionId); + return this.request({ + method: 'POST', + path: joinPaths(basePath, roleId, 'permission', permissionId), + }); + } + + public async removePermissionFromRole(params: RemovePermissionParams) { + const { roleId, permissionId } = params; + this.requireId(roleId); + this.requireId(permissionId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, roleId, 'permission', permissionId), + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index 4e377f290ee..b0098db07c9 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -7,6 +7,7 @@ export * from './EmailApi'; export * from './InterstitialApi'; export * from './InvitationApi'; export * from './OrganizationApi'; +export * from './OrganizationRoleApi'; export * from './OrganizationPermissionApi'; export * from './PhoneNumberApi'; export * from './RedirectUrlApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index a503ffc2362..f70bbb73582 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -7,6 +7,7 @@ import { InterstitialAPI, InvitationAPI, OrganizationAPI, + OrganizationRoleAPI, OrganizationPermissionAPI, PhoneNumberAPI, RedirectUrlAPI, @@ -30,6 +31,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { interstitial: new InterstitialAPI(request), invitations: new InvitationAPI(request), organizations: new OrganizationAPI(request), + organizationRoles: new OrganizationRoleAPI(request), organizationPermissions: new OrganizationPermissionAPI(request), phoneNumbers: new PhoneNumberAPI(request), redirectUrls: new RedirectUrlAPI(request), diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 41113c94708..3e5f8411d34 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -21,6 +21,7 @@ export enum ObjectType { Organization = 'organization', OrganizationInvitation = 'organization_invitation', OrganizationMembership = 'organization_membership', + Role = 'role', Permission = 'permission', PhoneNumber = 'phone_number', RedirectUrl = 'redirect_url', @@ -174,6 +175,16 @@ export interface OrganizationMembershipPublicUserDataJSON { user_id: string; } +export interface OrganizationRoleJSON extends ClerkResourceJSON { + object: ObjectType.Role; + name: string; + key: string; + description: string; + permissions: string[]; + created_at: number; + updated_at: number; +} + export interface PermissionJSON extends ClerkResourceJSON { object: ObjectType.Permission; id: string; diff --git a/packages/backend/src/api/resources/OrganizationRole.ts b/packages/backend/src/api/resources/OrganizationRole.ts new file mode 100644 index 00000000000..2bbc6bbff90 --- /dev/null +++ b/packages/backend/src/api/resources/OrganizationRole.ts @@ -0,0 +1,25 @@ +import type { OrganizationRoleJSON } from './JSON'; + +export class OrganizationRole { + constructor( + readonly id: string, + readonly name: string, + readonly key: string, + readonly description: string, + readonly permissions: string[], + readonly createdAt: number, + readonly updatedAt: number, + ) {} + + static fromJSON(data: OrganizationRoleJSON): OrganizationRole { + return new OrganizationRole( + data.id, + data.name, + data.key, + data.description, + data.permissions, + data.created_at, + data.updated_at, + ); + } +} diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 14d0367f98c..61473ae6666 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -22,6 +22,7 @@ export * from './Invitation'; export * from './JSON'; export * from './OauthAccessToken'; export * from './Organization'; +export * from './OrganizationRole'; export * from './Permission'; export * from './OrganizationInvitation'; export * from './OrganizationMembership'; From 46aa684882d9efd6e360dd19b3f565f182aa8e88 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 21 Nov 2023 10:49:40 +0200 Subject: [PATCH 2/6] test(backend): Update exports.test.ts --- packages/backend/src/exports.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/exports.test.ts b/packages/backend/src/exports.test.ts index 8ad994591b2..50ff9eec4be 100644 --- a/packages/backend/src/exports.test.ts +++ b/packages/backend/src/exports.test.ts @@ -24,6 +24,7 @@ export default (QUnit: QUnit) => { 'OrganizationInvitation', 'OrganizationMembership', 'OrganizationMembershipPublicUserData', + 'OrganizationRole', 'Permission', 'PhoneNumber', 'RedirectUrl', From 7ca0d9aa9b0640501a18dff37ea42d463b95ed15 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 21 Nov 2023 12:00:27 +0200 Subject: [PATCH 3/6] test(clerk-sdk-node): Update exports --- .../sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap | 2 ++ packages/sdk-node/src/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap index 2db4328b5f7..c526376f805 100644 --- a/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap @@ -20,6 +20,7 @@ exports[`module exports should not change unless explicitly set 1`] = ` "OrganizationInvitation", "OrganizationMembership", "OrganizationMembershipPublicUserData", + "OrganizationRole", "Permission", "PhoneNumber", "RedirectUrl", @@ -53,6 +54,7 @@ exports[`module exports should not change unless explicitly set 1`] = ` "invitations", "loadInterstitialFromLocal", "makeAuthObjectSerializable", + "organizationRoles", "organizationPermissions", "organizations", "phoneNumbers", diff --git a/packages/sdk-node/src/index.ts b/packages/sdk-node/src/index.ts index 4fb5cab8690..17cfb9e313f 100644 --- a/packages/sdk-node/src/index.ts +++ b/packages/sdk-node/src/index.ts @@ -26,6 +26,7 @@ const { emails, invitations, organizations, + organizationRoles, organizationPermissions, clients, allowlistIdentifiers, @@ -40,6 +41,7 @@ export { emails, invitations, organizations, + organizationRoles, organizationPermissions, phoneNumbers, sessions, From fcc4cef7024add32e8755e0f90038d40965e8dcb Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 21 Nov 2023 12:04:36 +0200 Subject: [PATCH 4/6] test(nextjs): Update exports.test.ts.snap --- .../src/server/__tests__/__snapshots__/exports.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index 03027bdabb1..7033e9bfbaa 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -18,6 +18,7 @@ exports[`/server public exports should not include a breaking change 1`] = ` "OrganizationInvitation", "OrganizationMembership", "OrganizationMembershipPublicUserData", + "OrganizationRole", "Permission", "PhoneNumber", "RedirectUrl", From ae9e7142ec2dea3819fdd67940be54d290c08833 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 21 Nov 2023 16:49:46 +0200 Subject: [PATCH 5/6] feat(backend): Address PR comments --- .../src/api/endpoints/OrganizationRoleApi.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/api/endpoints/OrganizationRoleApi.ts b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts index 7686fc14fc3..27a6cd55605 100644 --- a/packages/backend/src/api/endpoints/OrganizationRoleApi.ts +++ b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts @@ -1,6 +1,5 @@ import { joinPaths } from '../../util/path'; -import type { DeletedObject } from '../resources'; -import type { OrganizationRole } from '../resources/OrganizationRole'; +import type { DeletedObject, OrganizationRole } from '../resources'; import { AbstractAPI } from './AbstractApi'; const basePath = '/organizations_roles'; @@ -8,21 +7,62 @@ const basePath = '/organizations_roles'; type GetOrganizationRoleListParams = { limit?: number; offset?: number; + query?: string; + order_by?: string; }; type CreateParams = { + /** + * A name of a role in a readable friendly format. + * F.e. `Teacher` or `Administrator` + */ name: string; + + /** + * A unique identifier that represents the role. + * F.e. `org:administrator` + */ key: string; + + /** + * A brief description of what the role represents or its intended use. + */ description: string; + + /** + * An array of permission ids that will be assigned to this role. + */ permissions: string[]; }; type GetOrganizationRoleParams = { roleId: string }; type UpdateParams = { + /** + * A name of a role in a readable friendly format. + * F.e. `Teacher` or `Administrator` + * Passing undefined has no effect to the existing value. + */ name?: string; + + /** + * A unique identifier that represents the role. + * F.e. `org:administrator` + * Passing undefined has no effect to the existing value. + */ + key?: string; + + /** + * A brief description of what the role represents or its intended use. + * Passing undefined has no effect to the existing value. + */ description?: string; - //TODO(@pantelis): Will this override or union ? + + /** + * An array of permission ids that will be assigned to this role. + * Passing undefined has no effect to the permission that already exist. + * Passing an empty array will override the existing permissions. + */ permissions?: string[]; }; From 16d9e5dada0da12143e597ea7cf7c97e1bdc9f23 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 22 Nov 2023 15:29:50 +0200 Subject: [PATCH 6/6] chore(backend): Rename OrganizationRole to Role --- .../src/api/endpoints/OrganizationRoleApi.ts | 18 +++++++++--------- packages/backend/src/api/factory.ts | 2 +- packages/backend/src/api/resources/JSON.ts | 4 ++-- .../resources/{OrganizationRole.ts => Role.ts} | 13 +++++++------ packages/backend/src/api/resources/index.ts | 2 +- packages/backend/src/exports.test.ts | 2 +- .../__snapshots__/exports.test.ts.snap | 2 +- .../__snapshots__/exports.test.ts.snap | 4 ++-- 8 files changed, 24 insertions(+), 23 deletions(-) rename packages/backend/src/api/resources/{OrganizationRole.ts => Role.ts} (54%) diff --git a/packages/backend/src/api/endpoints/OrganizationRoleApi.ts b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts index 27a6cd55605..5385bcc0b7a 100644 --- a/packages/backend/src/api/endpoints/OrganizationRoleApi.ts +++ b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts @@ -1,10 +1,10 @@ import { joinPaths } from '../../util/path'; -import type { DeletedObject, OrganizationRole } from '../resources'; +import type { DeletedObject, Role } from '../resources'; import { AbstractAPI } from './AbstractApi'; const basePath = '/organizations_roles'; -type GetOrganizationRoleListParams = { +type GetRoleListParams = { limit?: number; offset?: number; query?: string; @@ -74,8 +74,8 @@ type RemovePermissionParams = { type AssignPermissionParams = RemovePermissionParams; export class OrganizationRoleAPI extends AbstractAPI { - public async getOrganizationRoleList(params?: GetOrganizationRoleListParams) { - return this.request({ + public async getOrganizationRoleList(params?: GetRoleListParams) { + return this.request({ method: 'GET', path: basePath, queryParams: params, @@ -83,7 +83,7 @@ export class OrganizationRoleAPI extends AbstractAPI { } public async createOrganizationRole(params: CreateParams) { - return this.request({ + return this.request({ method: 'POST', path: basePath, bodyParams: params, @@ -93,7 +93,7 @@ export class OrganizationRoleAPI extends AbstractAPI { public async getOrganizationRole(params: GetOrganizationRoleParams) { this.requireId(params.roleId); - return this.request({ + return this.request({ method: 'GET', path: joinPaths(basePath, params.roleId), }); @@ -101,7 +101,7 @@ export class OrganizationRoleAPI extends AbstractAPI { public async updateOrganizationRole(roleId: string, params: UpdateParams) { this.requireId(roleId); - return this.request({ + return this.request({ method: 'PATCH', path: joinPaths(basePath, roleId), bodyParams: params, @@ -120,7 +120,7 @@ export class OrganizationRoleAPI extends AbstractAPI { const { roleId, permissionId } = params; this.requireId(roleId); this.requireId(permissionId); - return this.request({ + return this.request({ method: 'POST', path: joinPaths(basePath, roleId, 'permission', permissionId), }); @@ -130,7 +130,7 @@ export class OrganizationRoleAPI extends AbstractAPI { const { roleId, permissionId } = params; this.requireId(roleId); this.requireId(permissionId); - return this.request({ + return this.request({ method: 'DELETE', path: joinPaths(basePath, roleId, 'permission', permissionId), }); diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index f70bbb73582..4da11471a5a 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -7,8 +7,8 @@ import { InterstitialAPI, InvitationAPI, OrganizationAPI, - OrganizationRoleAPI, OrganizationPermissionAPI, + OrganizationRoleAPI, PhoneNumberAPI, RedirectUrlAPI, SessionAPI, diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 3e5f8411d34..a094f177d48 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -175,12 +175,12 @@ export interface OrganizationMembershipPublicUserDataJSON { user_id: string; } -export interface OrganizationRoleJSON extends ClerkResourceJSON { +export interface RoleJSON extends ClerkResourceJSON { object: ObjectType.Role; name: string; key: string; description: string; - permissions: string[]; + permissions: PermissionJSON[]; created_at: number; updated_at: number; } diff --git a/packages/backend/src/api/resources/OrganizationRole.ts b/packages/backend/src/api/resources/Role.ts similarity index 54% rename from packages/backend/src/api/resources/OrganizationRole.ts rename to packages/backend/src/api/resources/Role.ts index 2bbc6bbff90..cd261531468 100644 --- a/packages/backend/src/api/resources/OrganizationRole.ts +++ b/packages/backend/src/api/resources/Role.ts @@ -1,23 +1,24 @@ -import type { OrganizationRoleJSON } from './JSON'; +import type { RoleJSON } from './JSON'; +import { Permission } from './Permission'; -export class OrganizationRole { +export class Role { constructor( readonly id: string, readonly name: string, readonly key: string, readonly description: string, - readonly permissions: string[], + readonly permissions: Permission[] = [], readonly createdAt: number, readonly updatedAt: number, ) {} - static fromJSON(data: OrganizationRoleJSON): OrganizationRole { - return new OrganizationRole( + static fromJSON(data: RoleJSON): Role { + return new Role( data.id, data.name, data.key, data.description, - data.permissions, + (data.permissions || []).map(x => Permission.fromJSON(x)), data.created_at, data.updated_at, ); diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 61473ae6666..6c95ba9cb39 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -22,7 +22,7 @@ export * from './Invitation'; export * from './JSON'; export * from './OauthAccessToken'; export * from './Organization'; -export * from './OrganizationRole'; +export * from './Role'; export * from './Permission'; export * from './OrganizationInvitation'; export * from './OrganizationMembership'; diff --git a/packages/backend/src/exports.test.ts b/packages/backend/src/exports.test.ts index 50ff9eec4be..ad6c9f415ef 100644 --- a/packages/backend/src/exports.test.ts +++ b/packages/backend/src/exports.test.ts @@ -24,10 +24,10 @@ export default (QUnit: QUnit) => { 'OrganizationInvitation', 'OrganizationMembership', 'OrganizationMembershipPublicUserData', - 'OrganizationRole', 'Permission', 'PhoneNumber', 'RedirectUrl', + 'Role', 'SMSMessage', 'Session', 'SignInToken', diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index 7033e9bfbaa..fb222287d83 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -18,10 +18,10 @@ exports[`/server public exports should not include a breaking change 1`] = ` "OrganizationInvitation", "OrganizationMembership", "OrganizationMembershipPublicUserData", - "OrganizationRole", "Permission", "PhoneNumber", "RedirectUrl", + "Role", "SMSMessage", "Session", "SignInToken", diff --git a/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap index c526376f805..b75a67f8cc0 100644 --- a/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap @@ -20,10 +20,10 @@ exports[`module exports should not change unless explicitly set 1`] = ` "OrganizationInvitation", "OrganizationMembership", "OrganizationMembershipPublicUserData", - "OrganizationRole", "Permission", "PhoneNumber", "RedirectUrl", + "Role", "SMSMessage", "Session", "SignInToken", @@ -54,8 +54,8 @@ exports[`module exports should not change unless explicitly set 1`] = ` "invitations", "loadInterstitialFromLocal", "makeAuthObjectSerializable", - "organizationRoles", "organizationPermissions", + "organizationRoles", "organizations", "phoneNumbers", "prunePrivateMetadata",