From 25e96879080969d5d273c1f8d85fe379cc7e5b62 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Mon, 1 Sep 2025 18:51:55 +0200 Subject: [PATCH 1/5] feat: add 'groups' namespace for managing OIDC groups --- src/groups/index.ts | 66 ++++++++++++++++++++++++++++++ src/groups/integration.test.ts | 75 ++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++ src/openapi/types.ts | 2 + src/roles/index.ts | 14 +++++++ src/roles/types.ts | 7 +++- src/roles/util.ts | 8 ++++ 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/groups/index.ts create mode 100644 src/groups/integration.test.ts diff --git a/src/groups/index.ts b/src/groups/index.ts new file mode 100644 index 00000000..cd5590d0 --- /dev/null +++ b/src/groups/index.ts @@ -0,0 +1,66 @@ +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 groupID [string] The group ID to get the roles for. + * @return A list of roles assigned tot he group. + */ + getAssignedRoles(groupID: string, includePermissions?: boolean): Promise>; + + /** + * Assign roles to a group specific to the configured OIDC's dynamic auth functionality. + * + * @param group_id [string] The group to assign the roles to. + * @param role_names [string[]] The names of the roles to assign to the group. + */ + assignRoles(groupID: string, roles: string | string[]): Promise; + /** + * Revoke roles from a group specific to the configured OIDC's dynamic auth functionality. + * + * @param group_id [string] The group to assign the roles to. + * @param role_names [string[]] The names of the roles to assign to the group. + */ + revokeRoles(groupID: string, roles: string | string[]): Promise; + /** + * Get the known group names specific to the configured OIDC's dynamic auth functionality. + * + * @return A list of known group names. + */ + getKnownGroupNames(): Promise; +} + +export const groups = (connection: ConnectionREST): Groups => ({ + oidc: { + getAssignedRoles: (groupID, includePermissions) => + connection + .get( + `/authz/groups/${encodeURIComponent(groupID)}/roles/oidc${ + includePermissions ? '?includeFullRoles=true' : '' + }` + ) + .then(Map.roles), + assignRoles: (groupID: string, roles: string | string[]): Promise => + connection.postEmpty(`/authz/groups/${encodeURIComponent(groupID)}/assign`, { + roles: Array.isArray(roles) ? roles : [roles], + groupType: 'oidc', + }), + revokeRoles: (groupID: string, roles: string | string[]): Promise => + connection.postEmpty(`/authz/groups/${encodeURIComponent(groupID)}/revoke`, { + roles: Array.isArray(roles) ? roles : [roles], + groupType: 'oidc', + }), + getKnownGroupNames: (): Promise => connection.get(`/authz/groups/oidc`), + }, +}); +export default groups; diff --git a/src/groups/integration.test.ts b/src/groups/integration.test.ts new file mode 100644 index 00000000..d518beea --- /dev/null +++ b/src/groups/integration.test.ts @@ -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([ + { 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 () => { + 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) => {}); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index 525ad05d..7c98f79f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 = { @@ -108,6 +109,7 @@ export interface WeaviateClient { cluster: Cluster; collections: Collections; oidcAuth?: OidcAuthenticator; + groups: Groups; roles: Roles; users: Users; @@ -230,6 +232,7 @@ async function client(params: ClientParams): Promise { 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() diff --git a/src/openapi/types.ts b/src/openapi/types.ts index ea746b9a..a5805c25 100644 --- a/src/openapi/types.ts +++ b/src/openapi/types.ts @@ -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]; diff --git a/src/roles/index.ts b/src/roles/index.ts index 96a52b57..d5c69dd2 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -1,6 +1,7 @@ import { ConnectionREST } from '../index.js'; import { WeaviateAssignedUser, + WeaviateGroupAssignment, Permission as WeaviatePermission, Role as WeaviateRole, } from '../openapi/types.js'; @@ -10,6 +11,7 @@ import { ClusterPermission, CollectionsPermission, DataPermission, + GroupAssignment, NodesPermission, Permission, PermissionsInput, @@ -102,6 +104,14 @@ export interface Roles { * @returns {Promise} 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; + + /** + * Get the IDs and group type of groups that assigned this role. + * + * @param {string} roleName The name of the role to check. + * @return {Promise} A promise that resolves to an array of group names assigned to this role. + */ + getGroupAssignments: (roleName: string) => Promise; } const roles = (connection: ConnectionREST): Roles => { @@ -147,6 +157,10 @@ const roles = (connection: ConnectionREST): Roles => { connection.postReturn(`/authz/roles/${roleName}/has-permission`, p) ) ).then((r) => r.every((b) => b)), + getGroupAssignments: (roleName: string) => + connection + .get(`/authz/roles/${roleName}/group-assignments`) + .then(Map.groupsAssignments), }; }; diff --git a/src/roles/types.ts b/src/roles/types.ts index 5b94d8fb..9242cd64 100644 --- a/src/roles/types.ts +++ b/src/roles/types.ts @@ -1,4 +1,4 @@ -import { Action, WeaviateUserType } from '../openapi/types.js'; +import { Action, WeaviateGroupType, WeaviateUserType } from '../openapi/types.js'; export type AliasAction = Extract< Action, @@ -31,6 +31,11 @@ export type UserAssignment = { userType: WeaviateUserType; }; +export type GroupAssignment = { + groupID: string; + groupType: WeaviateGroupType; +}; + export type AliasPermission = { alias: string; collection: string; diff --git a/src/roles/util.ts b/src/roles/util.ts index dd5f4f11..0f5f248a 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -1,6 +1,7 @@ import { WeaviateAssignedUser, WeaviateDBUser, + WeaviateGroupAssignment, Permission as WeaviatePermission, Role as WeaviateRole, WeaviateUser, @@ -17,6 +18,7 @@ import { CollectionsPermission, DataAction, DataPermission, + GroupAssignment, NodesAction, NodesPermission, Permission, @@ -157,6 +159,12 @@ export class Map { {} as Record ); + static groupsAssignments = (groups: WeaviateGroupAssignment[]): GroupAssignment[] => + groups.map((g) => ({ + groupID: g.groupId || '', + groupType: g.groupType, + })); + static users = (users: string[]): Record => users.reduce( (acc, user) => ({ From 4273a3051403c40c43604f4bf1862f5695dc7e82 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 2 Sep 2025 11:31:09 +0200 Subject: [PATCH 2/5] feat: add permissions for OIDC groups Note: the OpenAPI schema is manually modified to exclude 'db' group type, because it is not currently supported on the server. It will be removed from the next RC release (and GA too). Regenerate the schema once this PR is merged: https://github.com/weaviate/typescript-client/pull/343 --- src/groups/index.ts | 14 +++++------ src/openapi/schema.ts | 2 +- src/roles/index.ts | 47 ++++++++++++++++++++++++++++++++++- src/roles/integration.test.ts | 18 ++++++++++++++ src/roles/types.ts | 9 +++++++ src/roles/util.ts | 25 +++++++++++++++++++ 6 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/groups/index.ts b/src/groups/index.ts index cd5590d0..901177e0 100644 --- a/src/groups/index.ts +++ b/src/groups/index.ts @@ -13,29 +13,29 @@ export interface GroupsOIDC { /** * Get the roles assigned to a group specific to the configured OIDC's dynamic auth functionality. * - * @param groupID [string] The group ID to get the roles for. - * @return A list of roles assigned tot he group. + * @param {string} groupID The group ID to get the roles for. + * @returns {Promise>} A map of roles assigned to the group. */ getAssignedRoles(groupID: string, includePermissions?: boolean): Promise>; /** * Assign roles to a group specific to the configured OIDC's dynamic auth functionality. * - * @param group_id [string] The group to assign the roles to. - * @param role_names [string[]] The names of the roles to assign to the group. + * @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; /** * Revoke roles from a group specific to the configured OIDC's dynamic auth functionality. * - * @param group_id [string] The group to assign the roles to. - * @param role_names [string[]] The names of the roles to assign to the group. + * @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; /** * Get the known group names specific to the configured OIDC's dynamic auth functionality. * - * @return A list of known group names. + * @returns {Promise} A list of known group names. */ getKnownGroupNames(): Promise; } diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index 409fbfbe..c2cfab1c 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -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} diff --git a/src/roles/index.ts b/src/roles/index.ts index d5c69dd2..6760040f 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -12,6 +12,8 @@ import { CollectionsPermission, DataPermission, GroupAssignment, + GroupsAction, + GroupsPermission, NodesPermission, Permission, PermissionsInput, @@ -109,7 +111,7 @@ export interface Roles { * Get the IDs and group type of groups that assigned this role. * * @param {string} roleName The name of the role to check. - * @return {Promise} A promise that resolves to an array of group names assigned to this role. + * @returns {Promise} A promise that resolves to an array of group names assigned to this role. */ getGroupAssignments: (roleName: string) => Promise; } @@ -285,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. */ diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index c4d3ea33..ccddcf82 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -25,6 +25,7 @@ const emptyPermissions = { clusterPermissions: [], collectionsPermissions: [], dataPermissions: [], + groupsPermissions: [], nodesPermissions: [], rolesPermissions: [], tenantsPermissions: [], @@ -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({ diff --git a/src/roles/types.ts b/src/roles/types.ts index 9242cd64..2a757fe3 100644 --- a/src/roles/types.ts +++ b/src/roles/types.ts @@ -18,6 +18,7 @@ export type DataAction = Extract< Action, 'create_data' | 'delete_data' | 'read_data' | 'update_data' | 'manage_data' >; +export type GroupsAction = Extract; export type NodesAction = Extract; export type RolesAction = Extract; export type TenantsAction = Extract< @@ -62,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'; @@ -91,6 +98,7 @@ export type Role = { clusterPermissions: ClusterPermission[]; collectionsPermissions: CollectionsPermission[]; dataPermissions: DataPermission[]; + groupsPermissions: GroupsPermission[]; nodesPermissions: NodesPermission[]; rolesPermissions: RolesPermission[]; tenantsPermissions: TenantsPermission[]; @@ -103,6 +111,7 @@ export type Permission = | ClusterPermission | CollectionsPermission | DataPermission + | GroupsPermission | NodesPermission | RolesPermission | TenantsPermission diff --git a/src/roles/util.ts b/src/roles/util.ts index 0f5f248a..d1607e12 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -19,6 +19,8 @@ import { DataAction, DataPermission, GroupAssignment, + GroupsAction, + GroupsPermission, NodesAction, NodesPermission, Permission, @@ -67,6 +69,8 @@ export class PermissionGuards { 'read_data', 'update_data' ); + static isGroups = (permission: Permission): permission is GroupsPermission => + PermissionGuards.includes(permission, 'read_groups', 'assign_and_revoke_groups'); static isNodes = (permission: Permission): permission is NodesPermission => PermissionGuards.includes(permission, 'read_nodes'); static isRoles = (permission: Permission): permission is RolesPermission => @@ -129,6 +133,11 @@ export class Map { data: permission, action, })); + } else if (PermissionGuards.isGroups(permission)) { + return Array.from(permission.actions).map((action) => ({ + groups: { group: permission.groupID, groupType: permission.groupType }, + action, + })); } else if (PermissionGuards.isNodes(permission)) { return Array.from(permission.actions).map((action) => ({ nodes: permission, @@ -207,6 +216,7 @@ class PermissionsMapping { cluster: {}, collections: {}, data: {}, + groups: {}, nodes: {}, roles: {}, tenants: {}, @@ -230,6 +240,7 @@ class PermissionsMapping { clusterPermissions: Object.values(this.mappings.cluster), collectionsPermissions: Object.values(this.mappings.collections), dataPermissions: Object.values(this.mappings.data), + groupsPermissions: Object.values(this.mappings.groups), nodesPermissions: Object.values(this.mappings.nodes), rolesPermissions: Object.values(this.mappings.roles), tenantsPermissions: Object.values(this.mappings.tenants), @@ -286,6 +297,18 @@ class PermissionsMapping { } }; + private groups = (permission: WeaviatePermission) => { + if (permission.groups !== undefined) { + const { group, groupType } = permission.groups; + if (group === undefined) throw new Error('Group permission missing groupID'); + if (groupType === undefined) throw new Error('Group permission missing groupType'); + const key = `${groupType}#${group}`; + if (this.mappings.groups[key] === undefined) + this.mappings.groups[key] = { groupType, groupID: group, actions: [] }; + this.mappings.groups[key].actions.push(permission.action as GroupsAction); + } + }; + private nodes = (permission: WeaviatePermission) => { if (permission.nodes !== undefined) { let { collection } = permission.nodes; @@ -337,6 +360,7 @@ class PermissionsMapping { this.cluster(permission); this.collections(permission); this.data(permission); + this.groups(permission); this.nodes(permission); this.roles(permission); this.tenants(permission); @@ -350,6 +374,7 @@ type PermissionMappings = { cluster: Record; collections: Record; data: Record; + groups: Record; nodes: Record; roles: Record; tenants: Record; From 036efd71c825a2e77ddce39969c1d2ef931f5c8a Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 2 Sep 2025 11:32:48 +0200 Subject: [PATCH 3/5] test: await cleanup --- src/groups/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/groups/integration.test.ts b/src/groups/integration.test.ts index d518beea..1319c425 100644 --- a/src/groups/integration.test.ts +++ b/src/groups/integration.test.ts @@ -66,7 +66,7 @@ requireAtLeast(1, 32, 5).describe('Integration testing of the OIDC groups', () = }); it('cleanup', async () => { - makeClient().then((c) => { + 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) => {}); From 194c42e8c093c33776d7977dd1a819608f9dcc55 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 2 Sep 2025 12:05:24 +0200 Subject: [PATCH 4/5] chore: add missing param docstring --- src/groups/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/groups/index.ts b/src/groups/index.ts index 901177e0..810b2608 100644 --- a/src/groups/index.ts +++ b/src/groups/index.ts @@ -14,6 +14,7 @@ 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>} A map of roles assigned to the group. */ getAssignedRoles(groupID: string, includePermissions?: boolean): Promise>; From 66d291585cdbaa1c65f883eec495522a41a77919 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 2 Sep 2025 12:08:01 +0200 Subject: [PATCH 5/5] feat: quote user IDs when they're part of the URL --- src/users/index.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 3bae45f5..79e33a72 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -150,7 +150,7 @@ const users = (connection: ConnectionREST): Users => { return { getMyUser: () => connection.get('/users/own-info').then(Map.user), getAssignedRoles: (userId: string) => - connection.get(`/authz/users/${userId}/roles`).then(Map.roles), + connection.get(`/authz/users/${encodeURIComponent(userId)}/roles`).then(Map.roles), assignRoles: (roleNames: string | string[], userId: string) => base.assignRoles(roleNames, userId), revokeRoles: (roleNames: string | string[], userId: string) => base.revokeRoles(roleNames, userId), db: db(connection), @@ -182,30 +182,35 @@ const db = (connection: ConnectionREST): DBUsers => { ns.revokeRoles(roleNames, userId, { userType: 'db' }), create: (userId: string) => - connection.postReturn(`/users/db/${userId}`, null).then((resp) => resp.apikey), + connection + .postReturn(`/users/db/${encodeURIComponent(userId)}`, null) + .then((resp) => resp.apikey), delete: (userId: string) => connection - .delete(`/users/db/${userId}`, null) + .delete(`/users/db/${encodeURIComponent(userId)}`, null) .then(() => true) .catch(() => false), rotateKey: (userId: string) => connection - .postReturn(`/users/db/${userId}/rotate-key`, null) + .postReturn(`/users/db/${encodeURIComponent(userId)}/rotate-key`, null) .then((resp) => resp.apikey), activate: (userId: string) => connection - .postEmpty(`/users/db/${userId}/activate`, null) + .postEmpty(`/users/db/${encodeURIComponent(userId)}/activate`, null) .then(() => true) .catch(expectCode(409)), deactivate: (userId: string, opts?: DeactivateOptions) => connection - .postEmpty(`/users/db/${userId}/deactivate`, opts || null) + .postEmpty( + `/users/db/${encodeURIComponent(userId)}/deactivate`, + opts || null + ) .then(() => true) .catch(expectCode(409)), byName: (userId: string, opts?: GetUserOptions) => connection .get( - `/users/db/${userId}?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, + `/users/db/${encodeURIComponent(userId)}?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true ) .then(Map.dbUser), @@ -254,16 +259,18 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}?includeFullRoles=${opts?.includePermissions || false}` + `/authz/users/${encodeURIComponent(userId)}/roles/${userType}?includeFullRoles=${ + opts?.includePermissions || false + }` ) .then(Map.roles), assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => - connection.postEmpty(`/authz/users/${userId}/assign`, { + connection.postEmpty(`/authz/users/${encodeURIComponent(userId)}/assign`, { ...opts, roles: Array.isArray(roleNames) ? roleNames : [roleNames], }), revokeRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => - connection.postEmpty(`/authz/users/${userId}/revoke`, { + connection.postEmpty(`/authz/users/${encodeURIComponent(userId)}/revoke`, { ...opts, roles: Array.isArray(roleNames) ? roleNames : [roleNames], }),