Skip to content

Commit 955e998

Browse files
authored
feat(backend): Add Backend API calls for organization perms, roles (#8774)
1 parent 565a516 commit 955e998

15 files changed

Lines changed: 1188 additions & 1 deletion
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@clerk/backend": minor
3+
---
4+
5+
Add Backend API support for managing instance-level organization RBAC. `createClerkClient()` now exposes:
6+
7+
- `organizationPermissions` — list, get, create, update, and delete organization permissions.
8+
- `organizationRoles` — list, get, create, update, and delete organization roles, plus assign/remove a permission to/from a role.
9+
- `roleSets` — list, get, create, update, add roles to, replace a role in, and replace a role set.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { server, validateHeaders } from '../../mock-server';
5+
import { createBackendApiClient } from '../factory';
6+
7+
describe('OrganizationPermissionAPI', () => {
8+
const apiClient = createBackendApiClient({
9+
apiUrl: 'https://api.clerk.test',
10+
secretKey: 'deadbeef',
11+
});
12+
13+
const permissionId = 'perm_123';
14+
15+
const mockPermission = {
16+
object: 'permission',
17+
id: permissionId,
18+
name: 'Manage Billing',
19+
key: 'org:billing:manage',
20+
description: 'Allows managing billing',
21+
created_at: 1640995200,
22+
updated_at: 1640995200,
23+
};
24+
25+
const mockPaginatedResponse = {
26+
data: [mockPermission],
27+
total_count: 1,
28+
};
29+
30+
it('lists organization permissions with query parameters', async () => {
31+
server.use(
32+
http.get(
33+
'https://api.clerk.test/v1/organization_permissions',
34+
validateHeaders(({ request }) => {
35+
const url = new URL(request.url);
36+
expect(url.searchParams.get('limit')).toBe('10');
37+
expect(url.searchParams.get('offset')).toBe('5');
38+
expect(url.searchParams.get('query')).toBe('billing');
39+
expect(url.searchParams.get('order_by')).toBe('-created_at');
40+
return HttpResponse.json(mockPaginatedResponse);
41+
}),
42+
),
43+
);
44+
45+
const response = await apiClient.organizationPermissions.getOrganizationPermissionList({
46+
limit: 10,
47+
offset: 5,
48+
query: 'billing',
49+
orderBy: '-created_at',
50+
});
51+
52+
expect(response.data).toHaveLength(1);
53+
expect(response.totalCount).toBe(1);
54+
expect(response.data[0].key).toBe('org:billing:manage');
55+
});
56+
57+
it('fetches an organization permission by ID', async () => {
58+
server.use(
59+
http.get(
60+
`https://api.clerk.test/v1/organization_permissions/${permissionId}`,
61+
validateHeaders(() => HttpResponse.json(mockPermission)),
62+
),
63+
);
64+
65+
const response = await apiClient.organizationPermissions.getOrganizationPermission(permissionId);
66+
67+
expect(response.id).toBe(permissionId);
68+
expect(response.name).toBe('Manage Billing');
69+
});
70+
71+
it('creates an organization permission', async () => {
72+
const createParams = {
73+
name: 'Manage Billing',
74+
key: 'org:billing:manage',
75+
description: 'Allows managing billing',
76+
};
77+
78+
server.use(
79+
http.post(
80+
'https://api.clerk.test/v1/organization_permissions',
81+
validateHeaders(async ({ request }) => {
82+
const body = await request.json();
83+
expect(body).toEqual(createParams);
84+
return HttpResponse.json(mockPermission);
85+
}),
86+
),
87+
);
88+
89+
const response = await apiClient.organizationPermissions.createOrganizationPermission(createParams);
90+
91+
expect(response.id).toBe(permissionId);
92+
});
93+
94+
it('updates an organization permission', async () => {
95+
server.use(
96+
http.patch(
97+
`https://api.clerk.test/v1/organization_permissions/${permissionId}`,
98+
validateHeaders(async ({ request }) => {
99+
const body = await request.json();
100+
expect(body).toEqual({ name: 'Updated' });
101+
return HttpResponse.json({ ...mockPermission, name: 'Updated' });
102+
}),
103+
),
104+
);
105+
106+
const response = await apiClient.organizationPermissions.updateOrganizationPermission({
107+
permissionId,
108+
name: 'Updated',
109+
});
110+
111+
expect(response.name).toBe('Updated');
112+
});
113+
114+
it('deletes an organization permission', async () => {
115+
server.use(
116+
http.delete(
117+
`https://api.clerk.test/v1/organization_permissions/${permissionId}`,
118+
validateHeaders(() => HttpResponse.json({ object: 'permission', id: permissionId, deleted: true })),
119+
),
120+
);
121+
122+
const response = await apiClient.organizationPermissions.deleteOrganizationPermission(permissionId);
123+
124+
expect(response.id).toBe(permissionId);
125+
expect(response.deleted).toBe(true);
126+
});
127+
});
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { server, validateHeaders } from '../../mock-server';
5+
import { createBackendApiClient } from '../factory';
6+
7+
describe('OrganizationRoleAPI', () => {
8+
const apiClient = createBackendApiClient({
9+
apiUrl: 'https://api.clerk.test',
10+
secretKey: 'deadbeef',
11+
});
12+
13+
const roleId = 'role_123';
14+
const permissionId = 'perm_123';
15+
16+
const mockPermission = {
17+
object: 'permission',
18+
id: permissionId,
19+
name: 'Manage Billing',
20+
key: 'org:billing:manage',
21+
description: 'Allows managing billing',
22+
created_at: 1640995200,
23+
updated_at: 1640995200,
24+
};
25+
26+
const mockRole = {
27+
object: 'role',
28+
id: roleId,
29+
name: 'Billing Manager',
30+
key: 'org:billing_manager',
31+
description: null,
32+
is_creator_eligible: false,
33+
permissions: [mockPermission],
34+
created_at: 1640995200,
35+
updated_at: 1640995200,
36+
};
37+
38+
const mockPaginatedResponse = {
39+
data: [mockRole],
40+
total_count: 1,
41+
};
42+
43+
it('lists organization roles with query parameters', async () => {
44+
server.use(
45+
http.get(
46+
'https://api.clerk.test/v1/organization_roles',
47+
validateHeaders(({ request }) => {
48+
const url = new URL(request.url);
49+
expect(url.searchParams.get('limit')).toBe('20');
50+
expect(url.searchParams.get('query')).toBe('billing');
51+
return HttpResponse.json(mockPaginatedResponse);
52+
}),
53+
),
54+
);
55+
56+
const response = await apiClient.organizationRoles.getOrganizationRoleList({ limit: 20, query: 'billing' });
57+
58+
expect(response.data).toHaveLength(1);
59+
expect(response.totalCount).toBe(1);
60+
expect(response.data[0].permissions).toHaveLength(1);
61+
expect(response.data[0].permissions[0].key).toBe('org:billing:manage');
62+
});
63+
64+
it('fetches an organization role by ID', async () => {
65+
server.use(
66+
http.get(
67+
`https://api.clerk.test/v1/organization_roles/${roleId}`,
68+
validateHeaders(() => HttpResponse.json(mockRole)),
69+
),
70+
);
71+
72+
const response = await apiClient.organizationRoles.getOrganizationRole(roleId);
73+
74+
expect(response.id).toBe(roleId);
75+
expect(response.description).toBeNull();
76+
});
77+
78+
it('creates an organization role', async () => {
79+
const createParams = {
80+
name: 'Billing Manager',
81+
key: 'org:billing_manager',
82+
permissions: [permissionId],
83+
};
84+
85+
server.use(
86+
http.post(
87+
'https://api.clerk.test/v1/organization_roles',
88+
validateHeaders(async ({ request }) => {
89+
const body = await request.json();
90+
expect(body).toEqual(createParams);
91+
return HttpResponse.json(mockRole);
92+
}),
93+
),
94+
);
95+
96+
const response = await apiClient.organizationRoles.createOrganizationRole(createParams);
97+
98+
expect(response.id).toBe(roleId);
99+
});
100+
101+
it('updates an organization role', async () => {
102+
server.use(
103+
http.patch(
104+
`https://api.clerk.test/v1/organization_roles/${roleId}`,
105+
validateHeaders(async ({ request }) => {
106+
const body = await request.json();
107+
expect(body).toEqual({ name: 'Updated' });
108+
return HttpResponse.json({ ...mockRole, name: 'Updated' });
109+
}),
110+
),
111+
);
112+
113+
const response = await apiClient.organizationRoles.updateOrganizationRole({
114+
organizationRoleId: roleId,
115+
name: 'Updated',
116+
});
117+
118+
expect(response.name).toBe('Updated');
119+
});
120+
121+
it('deletes an organization role', async () => {
122+
server.use(
123+
http.delete(
124+
`https://api.clerk.test/v1/organization_roles/${roleId}`,
125+
validateHeaders(() => HttpResponse.json({ object: 'role', id: roleId, deleted: true })),
126+
),
127+
);
128+
129+
const response = await apiClient.organizationRoles.deleteOrganizationRole(roleId);
130+
131+
expect(response.id).toBe(roleId);
132+
expect(response.deleted).toBe(true);
133+
});
134+
135+
it('assigns a permission to an organization role', async () => {
136+
server.use(
137+
http.post(
138+
`https://api.clerk.test/v1/organization_roles/${roleId}/permissions/${permissionId}`,
139+
validateHeaders(() => HttpResponse.json(mockRole)),
140+
),
141+
);
142+
143+
const response = await apiClient.organizationRoles.assignPermissionToOrganizationRole({
144+
organizationRoleId: roleId,
145+
permissionId,
146+
});
147+
148+
expect(response.id).toBe(roleId);
149+
});
150+
151+
it('removes a permission from an organization role', async () => {
152+
server.use(
153+
http.delete(
154+
`https://api.clerk.test/v1/organization_roles/${roleId}/permissions/${permissionId}`,
155+
validateHeaders(() => HttpResponse.json(mockRole)),
156+
),
157+
);
158+
159+
const response = await apiClient.organizationRoles.removePermissionFromOrganizationRole({
160+
organizationRoleId: roleId,
161+
permissionId,
162+
});
163+
164+
expect(response.id).toBe(roleId);
165+
});
166+
});

0 commit comments

Comments
 (0)