Skip to content

Commit 25e9687

Browse files
committed
feat: add 'groups' namespace for managing OIDC groups
1 parent 36d6e63 commit 25e9687

File tree

7 files changed

+174
-1
lines changed

7 files changed

+174
-1
lines changed

src/groups/index.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 groupID [string] The group ID to get the roles for.
17+
* @return A list of roles assigned tot he group.
18+
*/
19+
getAssignedRoles(groupID: string, includePermissions?: boolean): Promise<Record<string, Role>>;
20+
21+
/**
22+
* Assign roles to a group specific to the configured OIDC's dynamic auth functionality.
23+
*
24+
* @param group_id [string] The group to assign the roles to.
25+
* @param role_names [string[]] The names of the roles to assign to the group.
26+
*/
27+
assignRoles(groupID: string, roles: string | string[]): Promise<void>;
28+
/**
29+
* Revoke roles from a group specific to the configured OIDC's dynamic auth functionality.
30+
*
31+
* @param group_id [string] The group to assign the roles to.
32+
* @param role_names [string[]] The names of the roles to assign to the group.
33+
*/
34+
revokeRoles(groupID: string, roles: string | string[]): Promise<void>;
35+
/**
36+
* Get the known group names specific to the configured OIDC's dynamic auth functionality.
37+
*
38+
* @return A list of known group names.
39+
*/
40+
getKnownGroupNames(): Promise<string[]>;
41+
}
42+
43+
export const groups = (connection: ConnectionREST): Groups => ({
44+
oidc: {
45+
getAssignedRoles: (groupID, includePermissions) =>
46+
connection
47+
.get<WeaviateRole[]>(
48+
`/authz/groups/${encodeURIComponent(groupID)}/roles/oidc${
49+
includePermissions ? '?includeFullRoles=true' : ''
50+
}`
51+
)
52+
.then(Map.roles),
53+
assignRoles: (groupID: string, roles: string | string[]): Promise<void> =>
54+
connection.postEmpty<any>(`/authz/groups/${encodeURIComponent(groupID)}/assign`, {
55+
roles: Array.isArray(roles) ? roles : [roles],
56+
groupType: 'oidc',
57+
}),
58+
revokeRoles: (groupID: string, roles: string | string[]): Promise<void> =>
59+
connection.postEmpty<any>(`/authz/groups/${encodeURIComponent(groupID)}/revoke`, {
60+
roles: Array.isArray(roles) ? roles : [roles],
61+
groupType: 'oidc',
62+
}),
63+
getKnownGroupNames: (): Promise<string[]> => connection.get(`/authz/groups/oidc`),
64+
},
65+
});
66+
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+
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/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: 14 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,7 @@ import {
1011
ClusterPermission,
1112
CollectionsPermission,
1213
DataPermission,
14+
GroupAssignment,
1315
NodesPermission,
1416
Permission,
1517
PermissionsInput,
@@ -102,6 +104,14 @@ export interface Roles {
102104
* @returns {Promise<boolean>} A promise that resolves to true if the role has the permissions, or false if it does not.
103105
*/
104106
hasPermissions: (roleName: string, permission: Permission | Permission[]) => Promise<boolean>;
107+
108+
/**
109+
* Get the IDs and group type of groups that assigned this role.
110+
*
111+
* @param {string} roleName The name of the role to check.
112+
* @return {Promise<GroupAssignment[]>} A promise that resolves to an array of group names assigned to this role.
113+
*/
114+
getGroupAssignments: (roleName: string) => Promise<GroupAssignment[]>;
105115
}
106116

107117
const roles = (connection: ConnectionREST): Roles => {
@@ -147,6 +157,10 @@ const roles = (connection: ConnectionREST): Roles => {
147157
connection.postReturn<WeaviatePermission, boolean>(`/authz/roles/${roleName}/has-permission`, p)
148158
)
149159
).then((r) => r.every((b) => b)),
160+
getGroupAssignments: (roleName: string) =>
161+
connection
162+
.get<WeaviateGroupAssignment[]>(`/authz/roles/${roleName}/group-assignments`)
163+
.then(Map.groupsAssignments),
150164
};
151165
};
152166

src/roles/types.ts

Lines changed: 6 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,
@@ -31,6 +31,11 @@ export type UserAssignment = {
3131
userType: WeaviateUserType;
3232
};
3333

34+
export type GroupAssignment = {
35+
groupID: string;
36+
groupType: WeaviateGroupType;
37+
};
38+
3439
export type AliasPermission = {
3540
alias: string;
3641
collection: string;

src/roles/util.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
WeaviateAssignedUser,
33
WeaviateDBUser,
4+
WeaviateGroupAssignment,
45
Permission as WeaviatePermission,
56
Role as WeaviateRole,
67
WeaviateUser,
@@ -17,6 +18,7 @@ import {
1718
CollectionsPermission,
1819
DataAction,
1920
DataPermission,
21+
GroupAssignment,
2022
NodesAction,
2123
NodesPermission,
2224
Permission,
@@ -157,6 +159,12 @@ export class Map {
157159
{} as Record<string, Role>
158160
);
159161

162+
static groupsAssignments = (groups: WeaviateGroupAssignment[]): GroupAssignment[] =>
163+
groups.map((g) => ({
164+
groupID: g.groupId || '',
165+
groupType: g.groupType,
166+
}));
167+
160168
static users = (users: string[]): Record<string, User> =>
161169
users.reduce(
162170
(acc, user) => ({

0 commit comments

Comments
 (0)