Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
513 changes: 459 additions & 54 deletions src/authorization/authorization.spec.ts

Large diffs are not rendered by default.

279 changes: 189 additions & 90 deletions src/authorization/authorization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { WorkOS } from '../workos';
import { AutoPaginatable } from '../common/utils/pagination';
import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';
import {
Role,
RoleList,
Expand All @@ -22,15 +24,11 @@ import {
RemoveOrganizationRolePermissionOptions,
Permission,
PermissionResponse,
PermissionList,
PermissionListResponse,
CreatePermissionOptions,
UpdatePermissionOptions,
ListPermissionsOptions,
AuthorizationResource,
AuthorizationResourceResponse,
AuthorizationResourceList,
AuthorizationResourceListResponse,
ListAuthorizationResourcesOptions,
GetAuthorizationResourceByExternalIdOptions,
UpdateAuthorizationResourceByExternalIdOptions,
Expand All @@ -45,12 +43,12 @@ import {
RemoveRoleAssignmentOptions,
RemoveRoleOptions,
RoleAssignment,
RoleAssignmentList,
RoleAssignmentListResponse,
RoleAssignmentResponse,
ListMembershipsForResourceByExternalIdOptions,
ListMembershipsForResourceOptions,
ListResourcesForMembershipOptions,
ListEffectivePermissionsOptions,
ListEffectivePermissionsByExternalIdOptions,
} from './interfaces';
import {
deserializeEnvironmentRole,
Expand All @@ -74,10 +72,11 @@ import {
serializeRemoveRoleOptions,
serializeListMembershipsForResourceOptions,
serializeListResourcesForMembershipOptions,
serializeListEffectivePermissionsOptions,
} from './serializers';
import {
AuthorizationOrganizationMembershipList,
AuthorizationOrganizationMembershipListResponse,
AuthorizationOrganizationMembership,
AuthorizationOrganizationMembershipResponse,
} from '../user-management/interfaces/organization-membership.interface';
import { deserializeAuthorizationOrganizationMembership } from '../user-management/serializers/organization-membership.serializer';

Expand Down Expand Up @@ -242,19 +241,23 @@ export class Authorization {

async listPermissions(
options?: ListPermissionsOptions,
): Promise<PermissionList> {
const { data } = await this.workos.get<PermissionListResponse>(
'/authorization/permissions',
{ query: options },
): Promise<AutoPaginatable<Permission>> {
return new AutoPaginatable(
await fetchAndDeserialize<PermissionResponse, Permission>(
this.workos,
'/authorization/permissions',
deserializePermission,
options,
),
(params) =>
fetchAndDeserialize<PermissionResponse, Permission>(
this.workos,
'/authorization/permissions',
deserializePermission,
params,
),
options,
);
Comment on lines 242 to 260
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

AutoPaginatable changes the runtime shape of these list responses.

This no longer returns a plain { object, data, listMetadata } object. In src/common/utils/pagination.ts:1-63, AutoPaginatable stores list, apiCall, and options as own instance fields, while data and listMetadata are prototype getters. So spreads, Object.keys(), and JSON serialization will behave differently for listPermissions() and the other converted authz list methods, which is a broader breaking change than the pagination fix itself. Please preserve the old enumerable shape or hide the internal fields before shipping this conversion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/authorization/authorization.ts` around lines 239 - 257, The
AutoPaginatable change made list responses non-enumerable and breaks callers
expecting a plain { object, data, listMetadata } shape; update listPermissions
(and other authz list methods) to return a plain enumerable object instead of
exposing AutoPaginatable internals: either (A) convert the AutoPaginatable
instance into a plain object before returning (e.g., construct and return {
object: 'list', data: autoPaginatable.list, listMetadata:
autoPaginatable.listMetadata, ...options }), or (B) modify AutoPaginatable in
src/common/utils/pagination.ts to expose enumerable data and listMetadata (or
implement a toJSON()/toPlainObject() that returns the original shape) and call
that from listPermissions; reference the listPermissions function and the
AutoPaginatable class in pagination.ts when making the change so the returned
value preserves the old enumerable shape.

return {
object: 'list',
data: data.data.map(deserializePermission),
listMetadata: {
before: data.list_metadata.before,
after: data.list_metadata.after,
},
};
}

async getPermission(slug: string): Promise<Permission> {
Expand Down Expand Up @@ -321,19 +324,31 @@ export class Authorization {

async listResources(
options: ListAuthorizationResourcesOptions = {},
): Promise<AuthorizationResourceList> {
const { data } = await this.workos.get<AuthorizationResourceListResponse>(
'/authorization/resources',
{ query: serializeListAuthorizationResourcesOptions(options) },
): Promise<AutoPaginatable<AuthorizationResource>> {
const serializedOptions =
serializeListAuthorizationResourcesOptions(options);
return new AutoPaginatable(
await fetchAndDeserialize<
AuthorizationResourceResponse,
AuthorizationResource
>(
this.workos,
'/authorization/resources',
deserializeAuthorizationResource,
serializedOptions,
),
(params) =>
fetchAndDeserialize<
AuthorizationResourceResponse,
AuthorizationResource
>(
this.workos,
'/authorization/resources',
deserializeAuthorizationResource,
params,
),
serializedOptions,
);
return {
object: 'list',
data: data.data.map(deserializeAuthorizationResource),
listMetadata: {
before: data.list_metadata.before,
after: data.list_metadata.after,
},
};
}

async getResourceByExternalId(
Expand Down Expand Up @@ -385,20 +400,25 @@ export class Authorization {

async listRoleAssignments(
options: ListRoleAssignmentsOptions,
): Promise<RoleAssignmentList> {
): Promise<AutoPaginatable<RoleAssignment>> {
const { organizationMembershipId, ...queryOptions } = options;
const { data } = await this.workos.get<RoleAssignmentListResponse>(
`/authorization/organization_memberships/${organizationMembershipId}/role_assignments`,
{ query: queryOptions },
const endpoint = `/authorization/organization_memberships/${organizationMembershipId}/role_assignments`;
return new AutoPaginatable(
await fetchAndDeserialize<RoleAssignmentResponse, RoleAssignment>(
this.workos,
endpoint,
deserializeRoleAssignment,
queryOptions,
),
(params) =>
fetchAndDeserialize<RoleAssignmentResponse, RoleAssignment>(
this.workos,
endpoint,
deserializeRoleAssignment,
params,
),
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<RoleAssignment> {
Expand Down Expand Up @@ -426,63 +446,142 @@ export class Authorization {

async listResourcesForMembership(
options: ListResourcesForMembershipOptions,
): Promise<AuthorizationResourceList> {
): Promise<AutoPaginatable<AuthorizationResource>> {
const { organizationMembershipId } = options;
const { data } = await this.workos.get<AuthorizationResourceListResponse>(
`/authorization/organization_memberships/${organizationMembershipId}/resources`,
{
query: serializeListResourcesForMembershipOptions(options),
},
const endpoint = `/authorization/organization_memberships/${organizationMembershipId}/resources`;
const serializedOptions =
serializeListResourcesForMembershipOptions(options);
return new AutoPaginatable(
await fetchAndDeserialize<
AuthorizationResourceResponse,
AuthorizationResource
>(
this.workos,
endpoint,
deserializeAuthorizationResource,
serializedOptions,
),
(params) =>
fetchAndDeserialize<
AuthorizationResourceResponse,
AuthorizationResource
>(this.workos, endpoint, deserializeAuthorizationResource, params),
serializedOptions,
);
return {
object: 'list',
data: data.data.map(deserializeAuthorizationResource),
listMetadata: {
before: data.list_metadata.before,
after: data.list_metadata.after,
},
};
}

async listMembershipsForResource(
options: ListMembershipsForResourceOptions,
): Promise<AuthorizationOrganizationMembershipList> {
): Promise<AutoPaginatable<AuthorizationOrganizationMembership>> {
const { resourceId } = options;
const { data } =
await this.workos.get<AuthorizationOrganizationMembershipListResponse>(
`/authorization/resources/${resourceId}/organization_memberships`,
{
query: serializeListMembershipsForResourceOptions(options),
},
);
return {
object: 'list',
data: data.data.map(deserializeAuthorizationOrganizationMembership),
listMetadata: {
before: data.list_metadata.before,
after: data.list_metadata.after,
},
};
const endpoint = `/authorization/resources/${resourceId}/organization_memberships`;
const serializedOptions =
serializeListMembershipsForResourceOptions(options);
return new AutoPaginatable(
await fetchAndDeserialize<
AuthorizationOrganizationMembershipResponse,
AuthorizationOrganizationMembership
>(
this.workos,
endpoint,
deserializeAuthorizationOrganizationMembership,
serializedOptions,
),
(params) =>
fetchAndDeserialize<
AuthorizationOrganizationMembershipResponse,
AuthorizationOrganizationMembership
>(
this.workos,
endpoint,
deserializeAuthorizationOrganizationMembership,
params,
),
serializedOptions,
);
}

async listMembershipsForResourceByExternalId(
options: ListMembershipsForResourceByExternalIdOptions,
): Promise<AuthorizationOrganizationMembershipList> {
): Promise<AutoPaginatable<AuthorizationOrganizationMembership>> {
const { organizationId, resourceTypeSlug, externalId } = options;
const { data } =
await this.workos.get<AuthorizationOrganizationMembershipListResponse>(
`/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}/organization_memberships`,
{
query: serializeListMembershipsForResourceOptions(options),
},
);
return {
object: 'list',
data: data.data.map(deserializeAuthorizationOrganizationMembership),
listMetadata: {
before: data.list_metadata.before,
after: data.list_metadata.after,
},
};
const endpoint = `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}/organization_memberships`;
const serializedOptions =
serializeListMembershipsForResourceOptions(options);
return new AutoPaginatable(
await fetchAndDeserialize<
AuthorizationOrganizationMembershipResponse,
AuthorizationOrganizationMembership
>(
this.workos,
endpoint,
deserializeAuthorizationOrganizationMembership,
serializedOptions,
),
(params) =>
fetchAndDeserialize<
AuthorizationOrganizationMembershipResponse,
AuthorizationOrganizationMembership
>(
this.workos,
endpoint,
deserializeAuthorizationOrganizationMembership,
params,
),
serializedOptions,
);
Comment on lines 447 to +532
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Strip path identifiers before serializing query options.

These helpers encode organizationMembershipId, resourceId, organizationId, resourceTypeSlug, and externalId into the URL, but still pass the full options bag to the serializers. That makes correctness depend on each serializer silently ignoring non-query fields. Destructure the path params first and serialize only the remaining query options so a later serializer change cannot start leaking duplicate query params.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/authorization/authorization.ts` around lines 444 - 529, The helpers
listResourcesForMembership, listMembershipsForResource, and
listMembershipsForResourceByExternalId currently pass the full options bag
(including path identifiers) into their serializer functions
(serializeListResourcesForMembershipOptions,
serializeListMembershipsForResourceOptions); update each function to destructure
and remove path params first—organizationMembershipId, resourceId,
organizationId, resourceTypeSlug, externalId—and then call the corresponding
serializer with only the remaining query options so path identifiers cannot be
encoded into the query string.

}

async listEffectivePermissions(
options: ListEffectivePermissionsOptions,
): Promise<AutoPaginatable<Permission>> {
const { organizationMembershipId, resourceId } = options;
const endpoint = `/authorization/resources/${resourceId}/organization_memberships/${organizationMembershipId}/permissions`;
const serializedOptions = serializeListEffectivePermissionsOptions(options);
return new AutoPaginatable(
await fetchAndDeserialize<PermissionResponse, Permission>(
this.workos,
endpoint,
deserializePermission,
serializedOptions,
),
(params) =>
fetchAndDeserialize<PermissionResponse, Permission>(
this.workos,
endpoint,
deserializePermission,
params,
),
serializedOptions,
);
}

async listEffectivePermissionsByExternalId(
options: ListEffectivePermissionsByExternalIdOptions,
): Promise<AutoPaginatable<Permission>> {
const {
organizationMembershipId,
organizationId,
resourceTypeSlug,
externalId,
} = options;
const endpoint = `/authorization/organizations/${organizationId}/resources/${resourceTypeSlug}/${externalId}/organization_memberships/${organizationMembershipId}/permissions`;
const serializedOptions = serializeListEffectivePermissionsOptions(options);
return new AutoPaginatable(
await fetchAndDeserialize<PermissionResponse, Permission>(
this.workos,
endpoint,
deserializePermission,
serializedOptions,
),
(params) =>
fetchAndDeserialize<PermissionResponse, Permission>(
this.workos,
endpoint,
deserializePermission,
params,
),
serializedOptions,
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
31 changes: 31 additions & 0 deletions src/authorization/fixtures/list-effective-permissions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"object": "list",
"data": [
{
"object": "permission",
"id": "perm_01HXYZ123ABC456DEF789GHI",
"slug": "documents:read",
"name": "Read Documents",
"description": "Allows reading documents",
"resource_type_slug": "document",
"system": false,
"created_at": "2024-01-15T08:00:00.000Z",
"updated_at": "2024-01-15T08:00:00.000Z"
},
{
"object": "permission",
"id": "perm_01HXYZ123ABC456DEF789GHJ",
"slug": "documents:edit",
"name": "Edit Documents",
"description": "Allows editing documents",
"resource_type_slug": "document",
"system": false,
"created_at": "2024-01-15T09:00:00.000Z",
"updated_at": "2024-01-15T09:00:00.000Z"
}
],
"list_metadata": {
"before": null,
"after": "perm_01HXYZ123ABC456DEF789GHJ"
}
}
Loading
Loading