Skip to content

Commit e36e54d

Browse files
authored
Merge pull request #343 from weaviate/feat/oidc-groups
feat: add 'groups' namespace for managing RBAC groups
2 parents ec3e0ed + 66d2915 commit e36e54d

File tree

10 files changed

+290
-12
lines changed

10 files changed

+290
-12
lines changed

src/groups/index.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import ConnectionREST from '../connection/http.js';
2+
import { Role } from '../roles/types.js';
3+
import { Map } from '../roles/util.js';
4+
5+
import { Role as WeaviateRole } from '../openapi/types.js';
6+
7+
export interface Groups {
8+
/** Manage roles of OIDC user groups. */
9+
oidc: GroupsOIDC;
10+
}
11+
12+
export interface GroupsOIDC {
13+
/**
14+
* Get the roles assigned to a group specific to the configured OIDC's dynamic auth functionality.
15+
*
16+
* @param {string} groupID The group ID to get the roles for.
17+
* @param {boolean} [includePermissions] Whether to include all associated permissions in the response.
18+
* @returns {Promise<Record<string, Role>>} A map of roles assigned to the group.
19+
*/
20+
getAssignedRoles(groupID: string, includePermissions?: boolean): Promise<Record<string, Role>>;
21+
22+
/**
23+
* Assign roles to a group specific to the configured OIDC's dynamic auth functionality.
24+
*
25+
* @param {string} groupID The group ID to get the roles for.
26+
* @param {string | string[]} roles The names of the roles to assign to the group.
27+
*/
28+
assignRoles(groupID: string, roles: string | string[]): Promise<void>;
29+
/**
30+
* Revoke roles from a group specific to the configured OIDC's dynamic auth functionality.
31+
*
32+
* @param {string} groupID The group ID to get the roles for.
33+
* @param {string | string[]} roles The names of the roles to revoke from the group.
34+
*/
35+
revokeRoles(groupID: string, roles: string | string[]): Promise<void>;
36+
/**
37+
* Get the known group names specific to the configured OIDC's dynamic auth functionality.
38+
*
39+
* @returns {Promise<string[]>} A list of known group names.
40+
*/
41+
getKnownGroupNames(): Promise<string[]>;
42+
}
43+
44+
export const groups = (connection: ConnectionREST): Groups => ({
45+
oidc: {
46+
getAssignedRoles: (groupID, includePermissions) =>
47+
connection
48+
.get<WeaviateRole[]>(
49+
`/authz/groups/${encodeURIComponent(groupID)}/roles/oidc${
50+
includePermissions ? '?includeFullRoles=true' : ''
51+
}`
52+
)
53+
.then(Map.roles),
54+
assignRoles: (groupID: string, roles: string | string[]): Promise<void> =>
55+
connection.postEmpty<any>(`/authz/groups/${encodeURIComponent(groupID)}/assign`, {
56+
roles: Array.isArray(roles) ? roles : [roles],
57+
groupType: 'oidc',
58+
}),
59+
revokeRoles: (groupID: string, roles: string | string[]): Promise<void> =>
60+
connection.postEmpty<any>(`/authz/groups/${encodeURIComponent(groupID)}/revoke`, {
61+
roles: Array.isArray(roles) ? roles : [roles],
62+
groupType: 'oidc',
63+
}),
64+
getKnownGroupNames: (): Promise<string[]> => connection.get(`/authz/groups/oidc`),
65+
},
66+
});
67+
export default groups;

src/groups/integration.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import weaviate, { ApiKey, GroupAssignment } from '..';
2+
import { requireAtLeast } from '../../test/version.js';
3+
4+
requireAtLeast(1, 32, 5).describe('Integration testing of the OIDC groups', () => {
5+
const makeClient = (key: string = 'admin-key') =>
6+
weaviate.connectToLocal({
7+
port: 8091,
8+
grpcPort: 50062,
9+
authCredentials: new ApiKey(key),
10+
});
11+
12+
it('should assign / get / revoke group roles', async () => {
13+
const client = await makeClient();
14+
const groupID = './assign-group';
15+
const roles = ['viewer', 'admin'];
16+
17+
await client.groups.oidc.revokeRoles(groupID, roles);
18+
await expect(client.groups.oidc.getAssignedRoles(groupID)).resolves.toEqual({});
19+
20+
await client.groups.oidc.assignRoles(groupID, roles);
21+
const assignedRoles = await client.groups.oidc.getAssignedRoles(groupID, true);
22+
expect(Object.keys(assignedRoles)).toEqual(expect.arrayContaining(roles));
23+
24+
await client.groups.oidc.revokeRoles(groupID, roles);
25+
await expect(client.groups.oidc.getAssignedRoles(groupID)).resolves.toEqual({});
26+
});
27+
28+
it('should get all known role groups', async () => {
29+
const client = await makeClient();
30+
const group1 = './group-1';
31+
const group2 = './group-2';
32+
33+
await client.groups.oidc.assignRoles(group1, 'viewer');
34+
await client.groups.oidc.assignRoles(group2, 'viewer');
35+
36+
await expect(client.groups.oidc.getKnownGroupNames()).resolves.toEqual(
37+
expect.arrayContaining([group1, group2])
38+
);
39+
40+
await client.groups.oidc.revokeRoles(group1, 'viewer');
41+
await client.groups.oidc.revokeRoles(group2, 'viewer');
42+
43+
await expect(client.groups.oidc.getKnownGroupNames()).resolves.toHaveLength(0);
44+
});
45+
46+
it('should get group assignments', async () => {
47+
const client = await makeClient();
48+
const roleName = 'test_group_assignements_role';
49+
await client.roles.delete(roleName).catch((e) => {});
50+
await client.roles.create(roleName, []).catch((e) => {});
51+
52+
await expect(client.roles.getGroupAssignments(roleName)).resolves.toHaveLength(0);
53+
54+
await client.groups.oidc.assignRoles('./group-1', roleName);
55+
await client.groups.oidc.assignRoles('./group-2', roleName);
56+
await expect(client.roles.getGroupAssignments(roleName)).resolves.toEqual(
57+
expect.arrayContaining<GroupAssignment>([
58+
{ groupID: './group-1', groupType: 'oidc' },
59+
{ groupID: './group-2', groupType: 'oidc' },
60+
])
61+
);
62+
63+
await client.groups.oidc.revokeRoles('./group-1', roleName);
64+
await client.groups.oidc.revokeRoles('./group-2', roleName);
65+
await expect(client.roles.getGroupAssignments(roleName)).resolves.toHaveLength(0);
66+
});
67+
68+
it('cleanup', async () => {
69+
await makeClient().then((c) => {
70+
c.groups.oidc.revokeRoles('./assign-group', ['viewer', 'admin']).catch((e) => {});
71+
c.groups.oidc.revokeRoles('./group-1', 'viewer').catch((e) => {});
72+
c.groups.oidc.revokeRoles('./group-2', 'viewer').catch((e) => {});
73+
});
74+
});
75+
});

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import weaviateV2 from './v2/index.js';
4141
import alias, { Aliases } from './alias/index.js';
4242
import filter from './collections/filters/index.js';
4343
import { ConsistencyLevel } from './data/replication.js';
44+
import groups, { Groups } from './groups/index.js';
4445
import users, { Users } from './users/index.js';
4546

4647
export type ProtocolParams = {
@@ -108,6 +109,7 @@ export interface WeaviateClient {
108109
cluster: Cluster;
109110
collections: Collections;
110111
oidcAuth?: OidcAuthenticator;
112+
groups: Groups;
111113
roles: Roles;
112114
users: Users;
113115

@@ -230,6 +232,7 @@ async function client(params: ClientParams): Promise<WeaviateClient> {
230232
backup: backup(connection),
231233
cluster: cluster(connection),
232234
collections: collections(connection, dbVersionSupport),
235+
groups: groups(connection),
233236
roles: roles(connection),
234237
users: users(connection),
235238
close: () => Promise.resolve(connection.close()), // hedge against future changes to add I/O to .close()

src/openapi/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ export interface definitions {
319319
* @description If the group contains OIDC or database users.
320320
* @enum {string}
321321
*/
322-
GroupType: 'db' | 'oidc';
322+
GroupType: 'oidc';
323323
/**
324324
* @description the type of user
325325
* @enum {string}

src/openapi/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export type Action = definitions['Permission']['action'];
7171
export type WeaviateUser = definitions['UserOwnInfo'];
7272
export type WeaviateDBUser = definitions['DBUserInfo'];
7373
export type WeaviateUserType = definitions['UserTypeOutput'];
74+
export type WeaviateGroupType = definitions['GroupType'];
75+
export type WeaviateGroupAssignment = operations['getGroupsForRole']['responses']['200']['schema'][0];
7476
export type WeaviateUserTypeInternal = definitions['UserTypeInput'];
7577
export type WeaviateUserTypeDB = definitions['DBUserInfo']['dbUserType'];
7678
export type WeaviateAssignedUser = operations['getUsersForRole']['responses']['200']['schema'][0];

src/roles/index.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ConnectionREST } from '../index.js';
22
import {
33
WeaviateAssignedUser,
4+
WeaviateGroupAssignment,
45
Permission as WeaviatePermission,
56
Role as WeaviateRole,
67
} from '../openapi/types.js';
@@ -10,6 +11,9 @@ import {
1011
ClusterPermission,
1112
CollectionsPermission,
1213
DataPermission,
14+
GroupAssignment,
15+
GroupsAction,
16+
GroupsPermission,
1317
NodesPermission,
1418
Permission,
1519
PermissionsInput,
@@ -102,6 +106,14 @@ export interface Roles {
102106
* @returns {Promise<boolean>} A promise that resolves to true if the role has the permissions, or false if it does not.
103107
*/
104108
hasPermissions: (roleName: string, permission: Permission | Permission[]) => Promise<boolean>;
109+
110+
/**
111+
* Get the IDs and group type of groups that assigned this role.
112+
*
113+
* @param {string} roleName The name of the role to check.
114+
* @returns {Promise<GroupAssignment[]>} A promise that resolves to an array of group names assigned to this role.
115+
*/
116+
getGroupAssignments: (roleName: string) => Promise<GroupAssignment[]>;
105117
}
106118

107119
const roles = (connection: ConnectionREST): Roles => {
@@ -147,6 +159,10 @@ const roles = (connection: ConnectionREST): Roles => {
147159
connection.postReturn<WeaviatePermission, boolean>(`/authz/roles/${roleName}/has-permission`, p)
148160
)
149161
).then((r) => r.every((b) => b)),
162+
getGroupAssignments: (roleName: string) =>
163+
connection
164+
.get<WeaviateGroupAssignment[]>(`/authz/roles/${roleName}/group-assignments`)
165+
.then(Map.groupsAssignments),
150166
};
151167
};
152168

@@ -271,6 +287,49 @@ export const permissions = {
271287
return out;
272288
});
273289
},
290+
/**
291+
* This namespace contains methods to create permissions specific to RBAC groups.
292+
*/
293+
groups: {
294+
/**
295+
* Create a set of permissions for 'oidc' groups.
296+
*
297+
* @param {string | string[]} args.groupID IDs of the groups with permissions.
298+
* @param {boolean} [args.read] Whether to allow reading groups. Defaults to `false`.
299+
* @param {boolean} [args.assignAndRevoke] Whether to allow changing group assignements. Defaults to `false`.
300+
* @returns {GroupsPermission[]} The permissions for managing groups.
301+
*/
302+
oidc: (args: {
303+
groupID: string | string[];
304+
read?: boolean;
305+
assignAndRevoke?: boolean;
306+
}): GroupsPermission[] => {
307+
const groups = Array.isArray(args.groupID) ? args.groupID : [args.groupID];
308+
const actions: GroupsAction[] = [];
309+
if (args.read) actions.push('read_groups');
310+
if (args.assignAndRevoke) actions.push('assign_and_revoke_groups');
311+
return groups.map((gid) => ({ groupID: gid, groupType: 'oidc', actions }));
312+
},
313+
/**
314+
* Create a set of permissions for 'db' groups.
315+
*
316+
* @param {string | string[]} args.groupID IDs of the groups with permissions.
317+
* @param {boolean} [args.read] Whether to allow reading groups. Defaults to `false`.
318+
* @param {boolean} [args.assignAndRevoke] Whether to allow changing group assignements. Defaults to `false`.
319+
* @returns {GroupsPermission[]} The permissions for managing groups.
320+
*/
321+
// db: (args: {
322+
// groupID: string | string[];
323+
// read?: boolean;
324+
// assignAndRevoke?: boolean;
325+
// }): GroupsPermission[] => {
326+
// const groups = Array.isArray(args.groupID) ? args.groupID : [args.groupID];
327+
// const actions: GroupsAction[] = [];
328+
// if (args.read) actions.push('read_groups');
329+
// if (args.assignAndRevoke) actions.push('assign_and_revoke_groups');
330+
// return groups.map((gid) => ({ groupID: gid, groupType: 'db', actions }));
331+
// },
332+
},
274333
/**
275334
* This namespace contains methods to create permissions specific to nodes.
276335
*/

src/roles/integration.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const emptyPermissions = {
2525
clusterPermissions: [],
2626
collectionsPermissions: [],
2727
dataPermissions: [],
28+
groupsPermissions: [],
2829
nodesPermissions: [],
2930
rolesPermissions: [],
3031
tenantsPermissions: [],
@@ -160,6 +161,23 @@ const testCases: TestCase[] = [
160161
],
161162
},
162163
},
164+
{
165+
roleName: 'groups-oidc',
166+
requireVersion: [1, 33, 0],
167+
permissions: weaviate.permissions.groups.oidc({
168+
groupID: ['G1', 'G2'],
169+
read: true,
170+
assignAndRevoke: true,
171+
}),
172+
expected: {
173+
name: 'groups-oidc',
174+
...emptyPermissions,
175+
groupsPermissions: [
176+
{ groupID: 'G1', groupType: 'oidc', actions: ['read_groups', 'assign_and_revoke_groups'] },
177+
{ groupID: 'G2', groupType: 'oidc', actions: ['read_groups', 'assign_and_revoke_groups'] },
178+
],
179+
},
180+
},
163181
{
164182
roleName: 'nodes-verbose',
165183
permissions: weaviate.permissions.nodes.verbose({

src/roles/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Action, WeaviateUserType } from '../openapi/types.js';
1+
import { Action, WeaviateGroupType, WeaviateUserType } from '../openapi/types.js';
22

33
export type AliasAction = Extract<
44
Action,
@@ -18,6 +18,7 @@ export type DataAction = Extract<
1818
Action,
1919
'create_data' | 'delete_data' | 'read_data' | 'update_data' | 'manage_data'
2020
>;
21+
export type GroupsAction = Extract<Action, 'read_groups' | 'assign_and_revoke_groups'>;
2122
export type NodesAction = Extract<Action, 'read_nodes'>;
2223
export type RolesAction = Extract<Action, 'create_roles' | 'read_roles' | 'update_roles' | 'delete_roles'>;
2324
export type TenantsAction = Extract<
@@ -31,6 +32,11 @@ export type UserAssignment = {
3132
userType: WeaviateUserType;
3233
};
3334

35+
export type GroupAssignment = {
36+
groupID: string;
37+
groupType: WeaviateGroupType;
38+
};
39+
3440
export type AliasPermission = {
3541
alias: string;
3642
collection: string;
@@ -57,6 +63,12 @@ export type DataPermission = {
5763
actions: DataAction[];
5864
};
5965

66+
export type GroupsPermission = {
67+
groupID: string;
68+
groupType: WeaviateGroupType;
69+
actions: GroupsAction[];
70+
};
71+
6072
export type NodesPermission = {
6173
collection: string;
6274
verbosity: 'verbose' | 'minimal';
@@ -86,6 +98,7 @@ export type Role = {
8698
clusterPermissions: ClusterPermission[];
8799
collectionsPermissions: CollectionsPermission[];
88100
dataPermissions: DataPermission[];
101+
groupsPermissions: GroupsPermission[];
89102
nodesPermissions: NodesPermission[];
90103
rolesPermissions: RolesPermission[];
91104
tenantsPermissions: TenantsPermission[];
@@ -98,6 +111,7 @@ export type Permission =
98111
| ClusterPermission
99112
| CollectionsPermission
100113
| DataPermission
114+
| GroupsPermission
101115
| NodesPermission
102116
| RolesPermission
103117
| TenantsPermission

0 commit comments

Comments
 (0)