diff --git a/src/authorization/authorization.spec.ts b/src/authorization/authorization.spec.ts index 92769db9c..f56044b09 100644 --- a/src/authorization/authorization.spec.ts +++ b/src/authorization/authorization.spec.ts @@ -8,8 +8,11 @@ import { import { WorkOS } from '../workos'; import environmentRoleFixture from './fixtures/environment-role.json'; import listEnvironmentRolesFixture from './fixtures/list-environment-roles.json'; +import organizationRoleFixture from './fixtures/organization-role.json'; +import listOrganizationRolesFixture from './fixtures/list-organization-roles.json'; const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); +const testOrgId = 'org_01HXYZ123ABC456DEF789ABC'; describe('Authorization', () => { beforeEach(() => fetch.resetMocks()); @@ -236,4 +239,210 @@ describe('Authorization', () => { ); }); }); + + describe('createOrganizationRole', () => { + it('creates an organization role', async () => { + fetchOnce(organizationRoleFixture, { status: 201 }); + + const role = await workos.authorization.createOrganizationRole( + testOrgId, + { + slug: 'org-admin', + name: 'Org Admin', + description: 'Organization administrator', + }, + ); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles`, + ); + expect(fetchBody()).toEqual({ + slug: 'org-admin', + name: 'Org Admin', + description: 'Organization administrator', + }); + expect(role).toMatchObject({ + object: 'role', + id: 'role_01HXYZ123ABC456DEF789ORG', + slug: 'org-admin', + name: 'Org Admin', + type: 'OrganizationRole', + }); + }); + }); + + describe('listOrganizationRoles', () => { + it('returns both environment and organization roles', async () => { + fetchOnce(listOrganizationRolesFixture); + + const { data, object } = + await workos.authorization.listOrganizationRoles(testOrgId); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles`, + ); + expect(object).toEqual('list'); + expect(data).toHaveLength(3); + expect(data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + slug: 'admin', + type: 'EnvironmentRole', + }), + expect.objectContaining({ + slug: 'org-admin', + type: 'OrganizationRole', + }), + expect.objectContaining({ + slug: 'org-member', + type: 'OrganizationRole', + }), + ]), + ); + }); + + it('passes expand parameter', async () => { + fetchOnce(listOrganizationRolesFixture); + + await workos.authorization.listOrganizationRoles(testOrgId, { + expand: 'permissions', + }); + + expect(fetchSearchParams()).toEqual({ + expand: 'permissions', + }); + }); + }); + + describe('getOrganizationRole', () => { + it('gets an organization role by slug', async () => { + fetchOnce(organizationRoleFixture); + + const role = await workos.authorization.getOrganizationRole( + testOrgId, + 'org-admin', + ); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles/org-admin`, + ); + expect(role).toMatchObject({ + object: 'role', + slug: 'org-admin', + type: 'OrganizationRole', + }); + }); + }); + + describe('updateOrganizationRole', () => { + it('updates an organization role', async () => { + const updatedRoleFixture = { + ...organizationRoleFixture, + name: 'Super Org Admin', + description: 'Updated description', + }; + fetchOnce(updatedRoleFixture); + + const role = await workos.authorization.updateOrganizationRole( + testOrgId, + 'org-admin', + { + name: 'Super Org Admin', + description: 'Updated description', + }, + ); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles/org-admin`, + ); + expect(fetchBody()).toEqual({ + name: 'Super Org Admin', + description: 'Updated description', + }); + expect(role).toMatchObject({ + name: 'Super Org Admin', + description: 'Updated description', + }); + }); + }); + + describe('deleteOrganizationRole', () => { + it('deletes an organization role', async () => { + fetchOnce({}, { status: 204 }); + + await workos.authorization.deleteOrganizationRole(testOrgId, 'org-admin'); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles/org-admin`, + ); + }); + }); + + describe('setOrganizationRolePermissions', () => { + it('sets permissions for an organization role', async () => { + const updatedRoleFixture = { + ...organizationRoleFixture, + permissions: ['org:read', 'org:write'], + }; + fetchOnce(updatedRoleFixture); + + const role = await workos.authorization.setOrganizationRolePermissions( + testOrgId, + 'org-admin', + ['org:read', 'org:write'], + ); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles/org-admin/permissions`, + ); + expect(fetchBody()).toEqual({ + permissions: ['org:read', 'org:write'], + }); + expect(role.permissions).toEqual( + expect.arrayContaining(['org:read', 'org:write']), + ); + }); + }); + + describe('addOrganizationRolePermission', () => { + it('adds a permission to an organization role', async () => { + const updatedRoleFixture = { + ...organizationRoleFixture, + permissions: ['org:manage', 'members:invite', 'billing:read'], + }; + fetchOnce(updatedRoleFixture); + + const role = await workos.authorization.addOrganizationRolePermission( + testOrgId, + 'org-admin', + 'billing:read', + ); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles/org-admin/permissions`, + ); + expect(fetchBody()).toEqual({ + slug: 'billing:read', + }); + expect(role.permissions).toEqual( + expect.arrayContaining(['billing:read']), + ); + }); + }); + + describe('removeOrganizationRolePermission', () => { + it('removes a permission from an organization role', async () => { + fetchOnce({}, { status: 200 }); + + await workos.authorization.removeOrganizationRolePermission( + testOrgId, + 'org-admin', + 'members:invite', + ); + + expect(fetchURL()).toContain( + `/authorization/organizations/${testOrgId}/roles/org-admin/permissions/members:invite`, + ); + }); + }); }); diff --git a/src/authorization/authorization.ts b/src/authorization/authorization.ts index 4c2e7d193..8324095ef 100644 --- a/src/authorization/authorization.ts +++ b/src/authorization/authorization.ts @@ -1,4 +1,10 @@ import { WorkOS } from '../workos'; +import { + Role, + RoleList, + OrganizationRoleResponse, + ListOrganizationRolesResponse, +} from '../roles/interfaces'; import { EnvironmentRole, EnvironmentRoleResponse, @@ -7,11 +13,19 @@ import { CreateEnvironmentRoleOptions, UpdateEnvironmentRoleOptions, ListEnvironmentRolesOptions, + OrganizationRole, + CreateOrganizationRoleOptions, + UpdateOrganizationRoleOptions, + ListOrganizationRolesOptions, } from './interfaces'; import { deserializeEnvironmentRole, serializeCreateEnvironmentRoleOptions, serializeUpdateEnvironmentRoleOptions, + deserializeRole, + deserializeOrganizationRole, + serializeCreateOrganizationRoleOptions, + serializeUpdateOrganizationRoleOptions, } from './serializers'; export class Authorization { @@ -79,4 +93,94 @@ export class Authorization { ); return deserializeEnvironmentRole(data); } + + async createOrganizationRole( + organizationId: string, + options: CreateOrganizationRoleOptions, + ): Promise { + const { data } = await this.workos.post( + `/authorization/organizations/${organizationId}/roles`, + serializeCreateOrganizationRoleOptions(options), + ); + return deserializeOrganizationRole(data); + } + + async listOrganizationRoles( + organizationId: string, + options?: ListOrganizationRolesOptions, + ): Promise { + const { data } = await this.workos.get( + `/authorization/organizations/${organizationId}/roles`, + { query: options }, + ); + return { + object: 'list', + data: data.data.map(deserializeRole), + }; + } + + async getOrganizationRole( + organizationId: string, + slug: string, + ): Promise { + const { data } = await this.workos.get( + `/authorization/organizations/${organizationId}/roles/${slug}`, + ); + return deserializeRole(data); + } + + async updateOrganizationRole( + organizationId: string, + slug: string, + options: UpdateOrganizationRoleOptions, + ): Promise { + const { data } = await this.workos.patch( + `/authorization/organizations/${organizationId}/roles/${slug}`, + serializeUpdateOrganizationRoleOptions(options), + ); + return deserializeOrganizationRole(data); + } + + async deleteOrganizationRole( + organizationId: string, + slug: string, + ): Promise { + await this.workos.delete( + `/authorization/organizations/${organizationId}/roles/${slug}`, + ); + } + + async setOrganizationRolePermissions( + organizationId: string, + slug: string, + permissions: string[], + ): Promise { + const { data } = await this.workos.put( + `/authorization/organizations/${organizationId}/roles/${slug}/permissions`, + { permissions }, + ); + return deserializeOrganizationRole(data); + } + + async addOrganizationRolePermission( + organizationId: string, + slug: string, + permissionSlug: string, + ): Promise { + const { data } = await this.workos.post( + `/authorization/organizations/${organizationId}/roles/${slug}/permissions`, + { slug: permissionSlug }, + ); + return deserializeOrganizationRole(data); + } + + async removeOrganizationRolePermission( + organizationId: string, + slug: string, + permissionSlug: string, + ): Promise { + await this.workos.delete( + `/authorization/organizations/${organizationId}/roles/${slug}/permissions/${permissionSlug}`, + ); + } } diff --git a/src/authorization/fixtures/list-organization-roles.json b/src/authorization/fixtures/list-organization-roles.json new file mode 100644 index 000000000..0533606d2 --- /dev/null +++ b/src/authorization/fixtures/list-organization-roles.json @@ -0,0 +1,38 @@ +{ + "object": "list", + "data": [ + { + "object": "role", + "id": "role_01HXYZ123ABC456DEF789ENV", + "name": "Admin", + "slug": "admin", + "description": "Environment-level admin role", + "permissions": ["users:read", "users:write"], + "type": "EnvironmentRole", + "created_at": "2024-01-15T08:00:00.000Z", + "updated_at": "2024-01-15T08:00:00.000Z" + }, + { + "object": "role", + "id": "role_01HXYZ123ABC456DEF789ORG", + "name": "Org Admin", + "slug": "org-admin", + "description": "Organization administrator", + "permissions": ["org:manage", "members:invite"], + "type": "OrganizationRole", + "created_at": "2024-01-15T09:30:00.000Z", + "updated_at": "2024-01-15T09:30:00.000Z" + }, + { + "object": "role", + "id": "role_01HXYZ123ABC456DEF789MEM", + "name": "Org Member", + "slug": "org-member", + "description": null, + "permissions": ["projects:read"], + "type": "OrganizationRole", + "created_at": "2024-01-15T10:00:00.000Z", + "updated_at": "2024-01-15T10:00:00.000Z" + } + ] +} diff --git a/src/authorization/fixtures/organization-role.json b/src/authorization/fixtures/organization-role.json new file mode 100644 index 000000000..0ae05d0cb --- /dev/null +++ b/src/authorization/fixtures/organization-role.json @@ -0,0 +1,11 @@ +{ + "object": "role", + "id": "role_01HXYZ123ABC456DEF789ORG", + "name": "Org Admin", + "slug": "org-admin", + "description": "Organization administrator", + "permissions": ["org:manage", "members:invite"], + "type": "OrganizationRole", + "created_at": "2024-01-15T09:30:00.000Z", + "updated_at": "2024-01-15T09:30:00.000Z" +} diff --git a/src/authorization/interfaces/create-organization-role-options.interface.ts b/src/authorization/interfaces/create-organization-role-options.interface.ts new file mode 100644 index 000000000..09b397e69 --- /dev/null +++ b/src/authorization/interfaces/create-organization-role-options.interface.ts @@ -0,0 +1,11 @@ +export interface CreateOrganizationRoleOptions { + slug: string; + name: string; + description?: string; +} + +export interface SerializedCreateOrganizationRoleOptions { + slug: string; + name: string; + description?: string; +} diff --git a/src/authorization/interfaces/index.ts b/src/authorization/interfaces/index.ts index 6def4d5bd..0f52d5312 100644 --- a/src/authorization/interfaces/index.ts +++ b/src/authorization/interfaces/index.ts @@ -2,3 +2,7 @@ export * from './environment-role.interface'; export * from './create-environment-role-options.interface'; export * from './update-environment-role-options.interface'; export * from './list-environment-roles-options.interface'; +export * from './organization-role.interface'; +export * from './create-organization-role-options.interface'; +export * from './update-organization-role-options.interface'; +export * from './list-organization-roles-options.interface'; diff --git a/src/authorization/interfaces/list-organization-roles-options.interface.ts b/src/authorization/interfaces/list-organization-roles-options.interface.ts new file mode 100644 index 000000000..6a6686c92 --- /dev/null +++ b/src/authorization/interfaces/list-organization-roles-options.interface.ts @@ -0,0 +1,3 @@ +export interface ListOrganizationRolesOptions { + expand?: 'permissions'; +} diff --git a/src/authorization/interfaces/organization-role.interface.ts b/src/authorization/interfaces/organization-role.interface.ts new file mode 100644 index 000000000..d17f850f4 --- /dev/null +++ b/src/authorization/interfaces/organization-role.interface.ts @@ -0,0 +1,11 @@ +export interface OrganizationRole { + object: 'role'; + id: string; + name: string; + slug: string; + description: string | null; + permissions: string[]; + type: 'OrganizationRole'; + createdAt: string; + updatedAt: string; +} diff --git a/src/authorization/interfaces/update-organization-role-options.interface.ts b/src/authorization/interfaces/update-organization-role-options.interface.ts new file mode 100644 index 000000000..a2fd4a314 --- /dev/null +++ b/src/authorization/interfaces/update-organization-role-options.interface.ts @@ -0,0 +1,9 @@ +export interface UpdateOrganizationRoleOptions { + name?: string; + description?: string | null; +} + +export interface SerializedUpdateOrganizationRoleOptions { + name?: string; + description?: string | null; +} diff --git a/src/authorization/serializers/create-organization-role-options.serializer.ts b/src/authorization/serializers/create-organization-role-options.serializer.ts new file mode 100644 index 000000000..f97bcebe0 --- /dev/null +++ b/src/authorization/serializers/create-organization-role-options.serializer.ts @@ -0,0 +1,12 @@ +import { + CreateOrganizationRoleOptions, + SerializedCreateOrganizationRoleOptions, +} from '../interfaces/create-organization-role-options.interface'; + +export const serializeCreateOrganizationRoleOptions = ( + options: CreateOrganizationRoleOptions, +): SerializedCreateOrganizationRoleOptions => ({ + slug: options.slug, + name: options.name, + description: options.description, +}); diff --git a/src/authorization/serializers/index.ts b/src/authorization/serializers/index.ts index 8aad90ac3..5e0f4c741 100644 --- a/src/authorization/serializers/index.ts +++ b/src/authorization/serializers/index.ts @@ -1,3 +1,6 @@ export * from './environment-role.serializer'; export * from './create-environment-role-options.serializer'; export * from './update-environment-role-options.serializer'; +export * from './organization-role.serializer'; +export * from './create-organization-role-options.serializer'; +export * from './update-organization-role-options.serializer'; diff --git a/src/authorization/serializers/organization-role.serializer.ts b/src/authorization/serializers/organization-role.serializer.ts new file mode 100644 index 000000000..3e20278d8 --- /dev/null +++ b/src/authorization/serializers/organization-role.serializer.ts @@ -0,0 +1,28 @@ +import { Role, OrganizationRoleResponse } from '../../roles/interfaces'; +import { OrganizationRole } from '../interfaces'; + +export const deserializeRole = (role: OrganizationRoleResponse): Role => ({ + object: role.object, + id: role.id, + name: role.name, + slug: role.slug, + description: role.description, + permissions: role.permissions, + type: role.type, + createdAt: role.created_at, + updatedAt: role.updated_at, +}); + +export const deserializeOrganizationRole = ( + role: OrganizationRoleResponse, +): OrganizationRole => ({ + object: role.object, + id: role.id, + name: role.name, + slug: role.slug, + description: role.description, + permissions: role.permissions, + type: 'OrganizationRole', + createdAt: role.created_at, + updatedAt: role.updated_at, +}); diff --git a/src/authorization/serializers/update-organization-role-options.serializer.ts b/src/authorization/serializers/update-organization-role-options.serializer.ts new file mode 100644 index 000000000..7bfc5571d --- /dev/null +++ b/src/authorization/serializers/update-organization-role-options.serializer.ts @@ -0,0 +1,11 @@ +import { + UpdateOrganizationRoleOptions, + SerializedUpdateOrganizationRoleOptions, +} from '../interfaces/update-organization-role-options.interface'; + +export const serializeUpdateOrganizationRoleOptions = ( + options: UpdateOrganizationRoleOptions, +): SerializedUpdateOrganizationRoleOptions => ({ + name: options.name, + description: options.description, +});