diff --git a/src/authorization/authorization.spec.ts b/src/authorization/authorization.spec.ts index 673a16ba2..33010beae 100644 --- a/src/authorization/authorization.spec.ts +++ b/src/authorization/authorization.spec.ts @@ -14,10 +14,14 @@ import permissionFixture from './fixtures/permission.json'; import listPermissionsFixture from './fixtures/list-permissions.json'; import authorizationResourceFixture from './fixtures/authorization-resource.json'; import listResourcesFixture from './fixtures/list-resources.json'; +import roleAssignmentFixture from './fixtures/role-assignment.json'; +import listRoleAssignmentsFixture from './fixtures/list-role-assignments.json'; + const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); const testOrgId = 'org_01HXYZ123ABC456DEF789ABC'; const testResourceId = 'authz_resource_01HXYZ123ABC456DEF789ABC'; const testOrgMembershipId = 'om_01HXYZ123ABC456DEF789ABC'; +const testRoleAssignmentId = 'role_assignment_01HXYZ123ABC456DEF789ABC'; describe('Authorization', () => { beforeEach(() => fetch.resetMocks()); @@ -1467,4 +1471,237 @@ describe('Authorization', () => { expect(body).toHaveProperty('resource_type_slug', 'document'); }); }); + + describe('listRoleAssignments', () => { + it('lists role assignments for an organization membership', async () => { + fetchOnce(listRoleAssignmentsFixture); + + const { data, object, listMetadata } = + await workos.authorization.listRoleAssignments({ + organizationMembershipId: testOrgMembershipId, + }); + + expect(fetchURL()).toContain( + `/authorization/organization_memberships/${testOrgMembershipId}/role_assignments`, + ); + expect(object).toEqual('list'); + expect(data).toHaveLength(1); + expect(data[0]).toMatchObject({ + object: 'role_assignment', + id: 'role_assignment_01HXYZ123ABC456DEF789ABC', + role: { slug: 'editor' }, + resource: { + id: 'resource_01HXYZ123ABC456DEF789XYZ', + externalId: 'doc-123', + resourceTypeSlug: 'document', + }, + createdAt: '2024-01-15T09:30:00.000Z', + updatedAt: '2024-01-15T09:30:00.000Z', + }); + expect(listMetadata).toEqual({ + before: null, + after: 'role_assignment_01HXYZ123ABC456DEF789ABC', + }); + }); + + it('passes pagination parameters', async () => { + fetchOnce(listRoleAssignmentsFixture); + + await workos.authorization.listRoleAssignments({ + organizationMembershipId: testOrgMembershipId, + limit: 10, + after: 'ra_cursor123', + order: 'desc', + }); + + expect(fetchSearchParams()).toEqual({ + limit: '10', + after: 'ra_cursor123', + order: 'desc', + }); + }); + + it('passes before pagination parameter', async () => { + fetchOnce(listRoleAssignmentsFixture); + + await workos.authorization.listRoleAssignments({ + organizationMembershipId: testOrgMembershipId, + limit: 10, + before: 'ra_cursor456', + order: 'asc', + }); + + expect(fetchSearchParams()).toEqual({ + limit: '10', + before: 'ra_cursor456', + order: 'asc', + }); + }); + }); + + describe('assignRole', () => { + it('assigns a role by resource ID', async () => { + fetchOnce(roleAssignmentFixture, { status: 201 }); + + const assignment = await workos.authorization.assignRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceId: testResourceId, + }); + + expect(fetchURL()).toContain( + `/authorization/organization_memberships/${testOrgMembershipId}/role_assignments`, + ); + expect(fetchBody()).toEqual({ + role_slug: 'editor', + resource_id: testResourceId, + }); + expect(assignment).toMatchObject({ + object: 'role_assignment', + id: 'role_assignment_01HXYZ123ABC456DEF789ABC', + role: { slug: 'editor' }, + resource: { + id: 'resource_01HXYZ123ABC456DEF789XYZ', + externalId: 'doc-123', + resourceTypeSlug: 'document', + }, + createdAt: '2024-01-15T09:30:00.000Z', + updatedAt: '2024-01-15T09:30:00.000Z', + }); + }); + + it('assigns a role by external ID & resourceTypeSlug', async () => { + fetchOnce(roleAssignmentFixture, { status: 201 }); + + const assignment = await workos.authorization.assignRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceExternalId: 'doc-123', + resourceTypeSlug: 'document', + }); + + expect(fetchBody()).toEqual({ + role_slug: 'editor', + resource_external_id: 'doc-123', + resource_type_slug: 'document', + }); + expect(assignment.resource.externalId).toBe('doc-123'); + expect(assignment.createdAt).toBe('2024-01-15T09:30:00.000Z'); + expect(assignment.updatedAt).toBe('2024-01-15T09:30:00.000Z'); + }); + + it('body only includes resource_id when resourceId is provided', async () => { + fetchOnce(roleAssignmentFixture, { status: 201 }); + + await workos.authorization.assignRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceId: testResourceId, + }); + + const body = fetchBody(); + expect(body).toHaveProperty('resource_id'); + expect(body).not.toHaveProperty('resource_external_id'); + expect(body).not.toHaveProperty('resource_type_slug'); + }); + + it('body only includes externalId and typeSlug when provided', async () => { + fetchOnce(roleAssignmentFixture, { status: 201 }); + + await workos.authorization.assignRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceExternalId: 'doc-123', + resourceTypeSlug: 'document', + }); + + const body = fetchBody(); + expect(body).not.toHaveProperty('resource_id'); + expect(body).toHaveProperty('resource_external_id'); + expect(body).toHaveProperty('resource_type_slug'); + }); + }); + + describe('removeRole', () => { + it('removes a role by resource ID', async () => { + fetchOnce({}, { status: 204 }); + + await workos.authorization.removeRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceId: testResourceId, + }); + + expect(fetchURL()).toContain( + `/authorization/organization_memberships/${testOrgMembershipId}/role_assignments`, + ); + expect(fetchSearchParams()).toEqual({ + role_slug: 'editor', + resource_id: testResourceId, + }); + }); + + it('removes a role by externalId and resourceTypeSlug', async () => { + fetchOnce({}, { status: 204 }); + + await workos.authorization.removeRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceExternalId: 'doc-123', + resourceTypeSlug: 'document', + }); + + expect(fetchSearchParams()).toEqual({ + role_slug: 'editor', + resource_external_id: 'doc-123', + resource_type_slug: 'document', + }); + }); + + it('query only includes resource_id when resourceId is provided', async () => { + fetchOnce(roleAssignmentFixture, { status: 201 }); + + await workos.authorization.removeRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceId: testResourceId, + }); + + const params = fetchSearchParams(); + expect(params).toHaveProperty('resource_id'); + expect(params).not.toHaveProperty('resource_external_id'); + expect(params).not.toHaveProperty('resource_type_slug'); + }); + + it('query only includes externalId and typeSlug when provided', async () => { + fetchOnce(roleAssignmentFixture, { status: 201 }); + + await workos.authorization.removeRole({ + organizationMembershipId: testOrgMembershipId, + roleSlug: 'editor', + resourceExternalId: 'doc-123', + resourceTypeSlug: 'document', + }); + + const params = fetchSearchParams(); + expect(params).not.toHaveProperty('resource_id'); + expect(params).toHaveProperty('resource_external_id'); + expect(params).toHaveProperty('resource_type_slug'); + }); + }); + + describe('removeRoleAssignment', () => { + it('removes a role assignment by ID', async () => { + fetchOnce({}, { status: 204 }); + + await workos.authorization.removeRoleAssignment({ + organizationMembershipId: testOrgMembershipId, + roleAssignmentId: testRoleAssignmentId, + }); + + expect(fetchURL()).toContain( + `/authorization/organization_memberships/${testOrgMembershipId}/role_assignments/${testRoleAssignmentId}`, + ); + }); + }); }); diff --git a/src/authorization/authorization.ts b/src/authorization/authorization.ts index f5edbf82b..39e6afe08 100644 --- a/src/authorization/authorization.ts +++ b/src/authorization/authorization.ts @@ -40,6 +40,14 @@ import { UpdateAuthorizationResourceOptions, AuthorizationCheckOptions, AuthorizationCheckResult, + AssignRoleOptions, + ListRoleAssignmentsOptions, + RemoveRoleAssignmentOptions, + RemoveRoleOptions, + RoleAssignment, + RoleAssignmentList, + RoleAssignmentListResponse, + RoleAssignmentResponse, } from './interfaces'; import { deserializeEnvironmentRole, @@ -58,6 +66,9 @@ import { serializeUpdateResourceByExternalIdOptions, serializeListAuthorizationResourcesOptions, serializeAuthorizationCheckOptions, + deserializeRoleAssignment, + serializeAssignRoleOptions, + serializeRemoveRoleOptions, } from './serializers'; export class Authorization { @@ -362,4 +373,45 @@ export class Authorization { ); return data; } + + async listRoleAssignments( + options: ListRoleAssignmentsOptions, + ): Promise { + const { organizationMembershipId, ...queryOptions } = options; + const { data } = await this.workos.get( + `/authorization/organization_memberships/${organizationMembershipId}/role_assignments`, + { query: queryOptions }, + ); + return { + object: 'list', + data: data.data.map(deserializeRoleAssignment), + listMetadata: { + before: data.list_metadata.before, + after: data.list_metadata.after, + }, + }; + } + + async assignRole(options: AssignRoleOptions): Promise { + const { data } = await this.workos.post( + `/authorization/organization_memberships/${options.organizationMembershipId}/role_assignments`, + serializeAssignRoleOptions(options), + ); + return deserializeRoleAssignment(data); + } + + async removeRole(options: RemoveRoleOptions): Promise { + await this.workos.delete( + `/authorization/organization_memberships/${options.organizationMembershipId}/role_assignments`, + serializeRemoveRoleOptions(options), + ); + } + + async removeRoleAssignment( + options: RemoveRoleAssignmentOptions, + ): Promise { + await this.workos.delete( + `/authorization/organization_memberships/${options.organizationMembershipId}/role_assignments/${options.roleAssignmentId}`, + ); + } } diff --git a/src/authorization/fixtures/list-role-assignments.json b/src/authorization/fixtures/list-role-assignments.json new file mode 100644 index 000000000..0def5cec0 --- /dev/null +++ b/src/authorization/fixtures/list-role-assignments.json @@ -0,0 +1,23 @@ +{ + "object": "list", + "data": [ + { + "object": "role_assignment", + "id": "role_assignment_01HXYZ123ABC456DEF789ABC", + "role": { + "slug": "editor" + }, + "resource": { + "id": "resource_01HXYZ123ABC456DEF789XYZ", + "external_id": "doc-123", + "resource_type_slug": "document" + }, + "created_at": "2024-01-15T09:30:00.000Z", + "updated_at": "2024-01-15T09:30:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": "role_assignment_01HXYZ123ABC456DEF789ABC" + } + } diff --git a/src/authorization/fixtures/role-assignment.json b/src/authorization/fixtures/role-assignment.json new file mode 100644 index 000000000..da7b9a6bc --- /dev/null +++ b/src/authorization/fixtures/role-assignment.json @@ -0,0 +1,14 @@ +{ + "object": "role_assignment", + "id": "role_assignment_01HXYZ123ABC456DEF789ABC", + "role": { + "slug": "editor" + }, + "resource": { + "id": "resource_01HXYZ123ABC456DEF789XYZ", + "external_id": "doc-123", + "resource_type_slug": "document" + }, + "created_at": "2024-01-15T09:30:00.000Z", + "updated_at": "2024-01-15T09:30:00.000Z" + } diff --git a/src/authorization/interfaces/assign-role-options.interface.ts b/src/authorization/interfaces/assign-role-options.interface.ts new file mode 100644 index 000000000..29d5b2211 --- /dev/null +++ b/src/authorization/interfaces/assign-role-options.interface.ts @@ -0,0 +1,26 @@ +import { + AuthorizationResourceIdentifierById, + AuthorizationResourceIdentifierByExternalId, +} from './authorization-resource-identifier.interface'; + +export interface BaseAssignRoleOptions { + organizationMembershipId: string; + roleSlug: string; +} + +export interface AssignRoleOptionsWithResourceId + extends BaseAssignRoleOptions, AuthorizationResourceIdentifierById {} + +export interface AssignRoleOptionsWithResourceExternalId + extends BaseAssignRoleOptions, AuthorizationResourceIdentifierByExternalId {} + +export type AssignRoleOptions = + | AssignRoleOptionsWithResourceId + | AssignRoleOptionsWithResourceExternalId; + +export interface SerializedAssignRoleOptions { + role_slug: string; + resource_id?: string; + resource_external_id?: string; + resource_type_slug?: string; +} diff --git a/src/authorization/interfaces/index.ts b/src/authorization/interfaces/index.ts index 313718190..7ec4859ef 100644 --- a/src/authorization/interfaces/index.ts +++ b/src/authorization/interfaces/index.ts @@ -20,3 +20,8 @@ export * from './update-authorization-resource-by-external-id-options.interface' export * from './delete-authorization-resource-by-external-id-options.interface'; export * from './delete-authorization-resource-options.interface'; export * from './authorization-resource-check.interface'; +export * from './role-assignment.interface'; +export * from './list-role-assignments-options.interface'; +export * from './assign-role-options.interface'; +export * from './remove-role-options.interface'; +export * from './remove-role-assignment-options.interface'; diff --git a/src/authorization/interfaces/list-role-assignments-options.interface.ts b/src/authorization/interfaces/list-role-assignments-options.interface.ts new file mode 100644 index 000000000..fd28b798b --- /dev/null +++ b/src/authorization/interfaces/list-role-assignments-options.interface.ts @@ -0,0 +1,4 @@ +import { PaginationOptions } from '../../common/interfaces/pagination-options.interface'; +export interface ListRoleAssignmentsOptions extends PaginationOptions { + organizationMembershipId: string; +} diff --git a/src/authorization/interfaces/remove-role-assignment-options.interface.ts b/src/authorization/interfaces/remove-role-assignment-options.interface.ts new file mode 100644 index 000000000..f874e0b56 --- /dev/null +++ b/src/authorization/interfaces/remove-role-assignment-options.interface.ts @@ -0,0 +1,4 @@ +export interface RemoveRoleAssignmentOptions { + organizationMembershipId: string; + roleAssignmentId: string; +} diff --git a/src/authorization/interfaces/remove-role-options.interface.ts b/src/authorization/interfaces/remove-role-options.interface.ts new file mode 100644 index 000000000..d260c5457 --- /dev/null +++ b/src/authorization/interfaces/remove-role-options.interface.ts @@ -0,0 +1,26 @@ +import { + AuthorizationResourceIdentifierById, + AuthorizationResourceIdentifierByExternalId, +} from './authorization-resource-identifier.interface'; + +export interface BaseRemoveRoleOptions { + organizationMembershipId: string; + roleSlug: string; +} + +export interface RemoveRoleOptionsWithResourceId + extends BaseRemoveRoleOptions, AuthorizationResourceIdentifierById {} + +export interface RemoveRoleOptionsWithResourceExternalId + extends BaseRemoveRoleOptions, AuthorizationResourceIdentifierByExternalId {} + +export type RemoveRoleOptions = + | RemoveRoleOptionsWithResourceId + | RemoveRoleOptionsWithResourceExternalId; + +export interface SerializedRemoveRoleOptions { + role_slug: string; + resource_id?: string; + resource_external_id?: string; + resource_type_slug?: string; +} diff --git a/src/authorization/interfaces/role-assignment.interface.ts b/src/authorization/interfaces/role-assignment.interface.ts new file mode 100644 index 000000000..83167ed9c --- /dev/null +++ b/src/authorization/interfaces/role-assignment.interface.ts @@ -0,0 +1,51 @@ +export interface RoleAssignmentRole { + slug: string; +} + +export interface RoleAssignmentResource { + id: string; + externalId: string; + resourceTypeSlug: string; +} + +export interface RoleAssignmentResourceResponse { + id: string; + external_id: string; + resource_type_slug: string; +} + +export interface RoleAssignment { + object: 'role_assignment'; + id: string; + role: RoleAssignmentRole; + resource: RoleAssignmentResource; + createdAt: string; + updatedAt: string; +} + +export interface RoleAssignmentResponse { + object: 'role_assignment'; + id: string; + role: RoleAssignmentRole; + resource: RoleAssignmentResourceResponse; + created_at: string; + updated_at: string; +} + +export interface RoleAssignmentList { + object: 'list'; + data: RoleAssignment[]; + listMetadata: { + before: string | null; + after: string | null; + }; +} + +export interface RoleAssignmentListResponse { + object: 'list'; + data: RoleAssignmentResponse[]; + list_metadata: { + before: string | null; + after: string | null; + }; +} diff --git a/src/authorization/serializers/assign-role-options.serializer.ts b/src/authorization/serializers/assign-role-options.serializer.ts new file mode 100644 index 000000000..115c9758e --- /dev/null +++ b/src/authorization/serializers/assign-role-options.serializer.ts @@ -0,0 +1,15 @@ +import { + AssignRoleOptions, + SerializedAssignRoleOptions, +} from '../interfaces/assign-role-options.interface'; + +export const serializeAssignRoleOptions = ( + options: AssignRoleOptions, +): SerializedAssignRoleOptions => ({ + role_slug: options.roleSlug, + ...('resourceId' in options && { resource_id: options.resourceId }), + ...('resourceExternalId' in options && { + resource_external_id: options.resourceExternalId, + resource_type_slug: options.resourceTypeSlug, + }), +}); diff --git a/src/authorization/serializers/index.ts b/src/authorization/serializers/index.ts index 3a5bd0e75..eba52f82f 100644 --- a/src/authorization/serializers/index.ts +++ b/src/authorization/serializers/index.ts @@ -13,3 +13,6 @@ export * from './update-authorization-resource-options.serializer'; export * from './update-authorization-resource-by-external-id-options.serializer'; export * from './list-authorization-resources-options.serializer'; export * from './authorization-check-options.serializer'; +export * from './role-assignment.serializer'; +export * from './assign-role-options.serializer'; +export * from './remove-role-options.serializer'; diff --git a/src/authorization/serializers/remove-role-options.serializer.ts b/src/authorization/serializers/remove-role-options.serializer.ts new file mode 100644 index 000000000..0efc08edc --- /dev/null +++ b/src/authorization/serializers/remove-role-options.serializer.ts @@ -0,0 +1,15 @@ +import { + RemoveRoleOptions, + SerializedRemoveRoleOptions, +} from '../interfaces/remove-role-options.interface'; + +export const serializeRemoveRoleOptions = ( + options: RemoveRoleOptions, +): SerializedRemoveRoleOptions => ({ + role_slug: options.roleSlug, + ...('resourceId' in options && { resource_id: options.resourceId }), + ...('resourceExternalId' in options && { + resource_external_id: options.resourceExternalId, + resource_type_slug: options.resourceTypeSlug, + }), +}); diff --git a/src/authorization/serializers/role-assignment.serializer.ts b/src/authorization/serializers/role-assignment.serializer.ts new file mode 100644 index 000000000..395fc722a --- /dev/null +++ b/src/authorization/serializers/role-assignment.serializer.ts @@ -0,0 +1,19 @@ +import { + RoleAssignment, + RoleAssignmentResponse, +} from '../interfaces/role-assignment.interface'; + +export const deserializeRoleAssignment = ( + response: RoleAssignmentResponse, +): RoleAssignment => ({ + object: response.object, + id: response.id, + role: response.role, + resource: { + id: response.resource.id, + externalId: response.resource.external_id, + resourceTypeSlug: response.resource.resource_type_slug, + }, + createdAt: response.created_at, + updatedAt: response.updated_at, +});