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
67 changes: 67 additions & 0 deletions src/groups/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import ConnectionREST from '../connection/http.js';
import { Role } from '../roles/types.js';
import { Map } from '../roles/util.js';

import { Role as WeaviateRole } from '../openapi/types.js';

export interface Groups {
/** Manage roles of OIDC user groups. */
oidc: GroupsOIDC;
}

export interface GroupsOIDC {
/**
* Get the roles assigned to a group specific to the configured OIDC's dynamic auth functionality.
*
* @param {string} groupID The group ID to get the roles for.
* @param {boolean} [includePermissions] Whether to include all associated permissions in the response.
* @returns {Promise<Record<string, Role>>} A map of roles assigned to the group.
*/
getAssignedRoles(groupID: string, includePermissions?: boolean): Promise<Record<string, Role>>;

/**
* Assign roles to a group specific to the configured OIDC's dynamic auth functionality.
*
* @param {string} groupID The group ID to get the roles for.
* @param {string | string[]} roles The names of the roles to assign to the group.
*/
assignRoles(groupID: string, roles: string | string[]): Promise<void>;
/**
* Revoke roles from a group specific to the configured OIDC's dynamic auth functionality.
*
* @param {string} groupID The group ID to get the roles for.
* @param {string | string[]} roles The names of the roles to revoke from the group.
*/
revokeRoles(groupID: string, roles: string | string[]): Promise<void>;
/**
* Get the known group names specific to the configured OIDC's dynamic auth functionality.
*
* @returns {Promise<string[]>} A list of known group names.
*/
getKnownGroupNames(): Promise<string[]>;
}

export const groups = (connection: ConnectionREST): Groups => ({
oidc: {
getAssignedRoles: (groupID, includePermissions) =>
connection
.get<WeaviateRole[]>(
`/authz/groups/${encodeURIComponent(groupID)}/roles/oidc${
includePermissions ? '?includeFullRoles=true' : ''
}`
)
.then(Map.roles),
assignRoles: (groupID: string, roles: string | string[]): Promise<void> =>
connection.postEmpty<any>(`/authz/groups/${encodeURIComponent(groupID)}/assign`, {
roles: Array.isArray(roles) ? roles : [roles],
groupType: 'oidc',
}),
revokeRoles: (groupID: string, roles: string | string[]): Promise<void> =>
connection.postEmpty<any>(`/authz/groups/${encodeURIComponent(groupID)}/revoke`, {
roles: Array.isArray(roles) ? roles : [roles],
groupType: 'oidc',
}),
getKnownGroupNames: (): Promise<string[]> => connection.get(`/authz/groups/oidc`),
},
});
export default groups;
75 changes: 75 additions & 0 deletions src/groups/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import weaviate, { ApiKey, GroupAssignment } from '..';
import { requireAtLeast } from '../../test/version.js';

requireAtLeast(1, 32, 5).describe('Integration testing of the OIDC groups', () => {
const makeClient = (key: string = 'admin-key') =>
weaviate.connectToLocal({
port: 8091,
grpcPort: 50062,
authCredentials: new ApiKey(key),
});

it('should assign / get / revoke group roles', async () => {
const client = await makeClient();
const groupID = './assign-group';
const roles = ['viewer', 'admin'];

await client.groups.oidc.revokeRoles(groupID, roles);
await expect(client.groups.oidc.getAssignedRoles(groupID)).resolves.toEqual({});

await client.groups.oidc.assignRoles(groupID, roles);
const assignedRoles = await client.groups.oidc.getAssignedRoles(groupID, true);
expect(Object.keys(assignedRoles)).toEqual(expect.arrayContaining(roles));

await client.groups.oidc.revokeRoles(groupID, roles);
await expect(client.groups.oidc.getAssignedRoles(groupID)).resolves.toEqual({});
});

it('should get all known role groups', async () => {
const client = await makeClient();
const group1 = './group-1';
const group2 = './group-2';

await client.groups.oidc.assignRoles(group1, 'viewer');
await client.groups.oidc.assignRoles(group2, 'viewer');

await expect(client.groups.oidc.getKnownGroupNames()).resolves.toEqual(
expect.arrayContaining([group1, group2])
);

await client.groups.oidc.revokeRoles(group1, 'viewer');
await client.groups.oidc.revokeRoles(group2, 'viewer');

await expect(client.groups.oidc.getKnownGroupNames()).resolves.toHaveLength(0);
});

it('should get group assignments', async () => {
const client = await makeClient();
const roleName = 'test_group_assignements_role';
await client.roles.delete(roleName).catch((e) => {});
await client.roles.create(roleName, []).catch((e) => {});

await expect(client.roles.getGroupAssignments(roleName)).resolves.toHaveLength(0);

await client.groups.oidc.assignRoles('./group-1', roleName);
await client.groups.oidc.assignRoles('./group-2', roleName);
await expect(client.roles.getGroupAssignments(roleName)).resolves.toEqual(
expect.arrayContaining<GroupAssignment>([
{ groupID: './group-1', groupType: 'oidc' },
{ groupID: './group-2', groupType: 'oidc' },
])
);

await client.groups.oidc.revokeRoles('./group-1', roleName);
await client.groups.oidc.revokeRoles('./group-2', roleName);
await expect(client.roles.getGroupAssignments(roleName)).resolves.toHaveLength(0);
});

it('cleanup', async () => {
await makeClient().then((c) => {
c.groups.oidc.revokeRoles('./assign-group', ['viewer', 'admin']).catch((e) => {});
c.groups.oidc.revokeRoles('./group-1', 'viewer').catch((e) => {});
c.groups.oidc.revokeRoles('./group-2', 'viewer').catch((e) => {});
});
});
});
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import weaviateV2 from './v2/index.js';
import alias, { Aliases } from './alias/index.js';
import filter from './collections/filters/index.js';
import { ConsistencyLevel } from './data/replication.js';
import groups, { Groups } from './groups/index.js';
import users, { Users } from './users/index.js';

export type ProtocolParams = {
Expand Down Expand Up @@ -108,6 +109,7 @@ export interface WeaviateClient {
cluster: Cluster;
collections: Collections;
oidcAuth?: OidcAuthenticator;
groups: Groups;
roles: Roles;
users: Users;

Expand Down Expand Up @@ -230,6 +232,7 @@ async function client(params: ClientParams): Promise<WeaviateClient> {
backup: backup(connection),
cluster: cluster(connection),
collections: collections(connection, dbVersionSupport),
groups: groups(connection),
roles: roles(connection),
users: users(connection),
close: () => Promise.resolve(connection.close()), // hedge against future changes to add I/O to .close()
Expand Down
2 changes: 1 addition & 1 deletion src/openapi/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ export interface definitions {
* @description If the group contains OIDC or database users.
* @enum {string}
*/
GroupType: 'db' | 'oidc';
GroupType: 'oidc';
/**
* @description the type of user
* @enum {string}
Expand Down
2 changes: 2 additions & 0 deletions src/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export type Action = definitions['Permission']['action'];
export type WeaviateUser = definitions['UserOwnInfo'];
export type WeaviateDBUser = definitions['DBUserInfo'];
export type WeaviateUserType = definitions['UserTypeOutput'];
export type WeaviateGroupType = definitions['GroupType'];
export type WeaviateGroupAssignment = operations['getGroupsForRole']['responses']['200']['schema'][0];
export type WeaviateUserTypeInternal = definitions['UserTypeInput'];
export type WeaviateUserTypeDB = definitions['DBUserInfo']['dbUserType'];
export type WeaviateAssignedUser = operations['getUsersForRole']['responses']['200']['schema'][0];
Expand Down
59 changes: 59 additions & 0 deletions src/roles/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ConnectionREST } from '../index.js';
import {
WeaviateAssignedUser,
WeaviateGroupAssignment,
Permission as WeaviatePermission,
Role as WeaviateRole,
} from '../openapi/types.js';
Expand All @@ -10,6 +11,9 @@ import {
ClusterPermission,
CollectionsPermission,
DataPermission,
GroupAssignment,
GroupsAction,
GroupsPermission,
NodesPermission,
Permission,
PermissionsInput,
Expand Down Expand Up @@ -102,6 +106,14 @@ export interface Roles {
* @returns {Promise<boolean>} A promise that resolves to true if the role has the permissions, or false if it does not.
*/
hasPermissions: (roleName: string, permission: Permission | Permission[]) => Promise<boolean>;

/**
* Get the IDs and group type of groups that assigned this role.
*
* @param {string} roleName The name of the role to check.
* @returns {Promise<GroupAssignment[]>} A promise that resolves to an array of group names assigned to this role.
*/
getGroupAssignments: (roleName: string) => Promise<GroupAssignment[]>;
}

const roles = (connection: ConnectionREST): Roles => {
Expand Down Expand Up @@ -147,6 +159,10 @@ const roles = (connection: ConnectionREST): Roles => {
connection.postReturn<WeaviatePermission, boolean>(`/authz/roles/${roleName}/has-permission`, p)
)
).then((r) => r.every((b) => b)),
getGroupAssignments: (roleName: string) =>
connection
.get<WeaviateGroupAssignment[]>(`/authz/roles/${roleName}/group-assignments`)
.then(Map.groupsAssignments),
};
};

Expand Down Expand Up @@ -271,6 +287,49 @@ export const permissions = {
return out;
});
},
/**
* This namespace contains methods to create permissions specific to RBAC groups.
*/
groups: {
/**
* Create a set of permissions for 'oidc' groups.
*
* @param {string | string[]} args.groupID IDs of the groups with permissions.
* @param {boolean} [args.read] Whether to allow reading groups. Defaults to `false`.
* @param {boolean} [args.assignAndRevoke] Whether to allow changing group assignements. Defaults to `false`.
* @returns {GroupsPermission[]} The permissions for managing groups.
*/
oidc: (args: {
groupID: string | string[];
read?: boolean;
assignAndRevoke?: boolean;
}): GroupsPermission[] => {
const groups = Array.isArray(args.groupID) ? args.groupID : [args.groupID];
const actions: GroupsAction[] = [];
if (args.read) actions.push('read_groups');
if (args.assignAndRevoke) actions.push('assign_and_revoke_groups');
return groups.map((gid) => ({ groupID: gid, groupType: 'oidc', actions }));
},
/**
* Create a set of permissions for 'db' groups.
*
* @param {string | string[]} args.groupID IDs of the groups with permissions.
* @param {boolean} [args.read] Whether to allow reading groups. Defaults to `false`.
* @param {boolean} [args.assignAndRevoke] Whether to allow changing group assignements. Defaults to `false`.
* @returns {GroupsPermission[]} The permissions for managing groups.
*/
// db: (args: {
// groupID: string | string[];
// read?: boolean;
// assignAndRevoke?: boolean;
// }): GroupsPermission[] => {
// const groups = Array.isArray(args.groupID) ? args.groupID : [args.groupID];
// const actions: GroupsAction[] = [];
// if (args.read) actions.push('read_groups');
// if (args.assignAndRevoke) actions.push('assign_and_revoke_groups');
// return groups.map((gid) => ({ groupID: gid, groupType: 'db', actions }));
// },
},
/**
* This namespace contains methods to create permissions specific to nodes.
*/
Expand Down
18 changes: 18 additions & 0 deletions src/roles/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const emptyPermissions = {
clusterPermissions: [],
collectionsPermissions: [],
dataPermissions: [],
groupsPermissions: [],
nodesPermissions: [],
rolesPermissions: [],
tenantsPermissions: [],
Expand Down Expand Up @@ -160,6 +161,23 @@ const testCases: TestCase[] = [
],
},
},
{
roleName: 'groups-oidc',
requireVersion: [1, 33, 0],
permissions: weaviate.permissions.groups.oidc({
groupID: ['G1', 'G2'],
read: true,
assignAndRevoke: true,
}),
expected: {
name: 'groups-oidc',
...emptyPermissions,
groupsPermissions: [
{ groupID: 'G1', groupType: 'oidc', actions: ['read_groups', 'assign_and_revoke_groups'] },
{ groupID: 'G2', groupType: 'oidc', actions: ['read_groups', 'assign_and_revoke_groups'] },
],
},
},
{
roleName: 'nodes-verbose',
permissions: weaviate.permissions.nodes.verbose({
Expand Down
16 changes: 15 additions & 1 deletion src/roles/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, WeaviateUserType } from '../openapi/types.js';
import { Action, WeaviateGroupType, WeaviateUserType } from '../openapi/types.js';

export type AliasAction = Extract<
Action,
Expand All @@ -18,6 +18,7 @@ export type DataAction = Extract<
Action,
'create_data' | 'delete_data' | 'read_data' | 'update_data' | 'manage_data'
>;
export type GroupsAction = Extract<Action, 'read_groups' | 'assign_and_revoke_groups'>;
export type NodesAction = Extract<Action, 'read_nodes'>;
export type RolesAction = Extract<Action, 'create_roles' | 'read_roles' | 'update_roles' | 'delete_roles'>;
export type TenantsAction = Extract<
Expand All @@ -31,6 +32,11 @@ export type UserAssignment = {
userType: WeaviateUserType;
};

export type GroupAssignment = {
groupID: string;
groupType: WeaviateGroupType;
};

export type AliasPermission = {
alias: string;
collection: string;
Expand All @@ -57,6 +63,12 @@ export type DataPermission = {
actions: DataAction[];
};

export type GroupsPermission = {
groupID: string;
groupType: WeaviateGroupType;
actions: GroupsAction[];
};

export type NodesPermission = {
collection: string;
verbosity: 'verbose' | 'minimal';
Expand Down Expand Up @@ -86,6 +98,7 @@ export type Role = {
clusterPermissions: ClusterPermission[];
collectionsPermissions: CollectionsPermission[];
dataPermissions: DataPermission[];
groupsPermissions: GroupsPermission[];
nodesPermissions: NodesPermission[];
rolesPermissions: RolesPermission[];
tenantsPermissions: TenantsPermission[];
Expand All @@ -98,6 +111,7 @@ export type Permission =
| ClusterPermission
| CollectionsPermission
| DataPermission
| GroupsPermission
| NodesPermission
| RolesPermission
| TenantsPermission
Expand Down
Loading