diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d63cd1f2..407bbaea 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ env: WEAVIATE_126: 1.26.14 WEAVIATE_127: 1.27.11 WEAVIATE_128: 1.28.4 - WEAVIATE_129: 1.29.0-rc.0 + WEAVIATE_129: 1.29.0-rc.0-a8c0bce jobs: checks: diff --git a/src/index.ts b/src/index.ts index 101bc14e..653b9c60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,7 @@ import { LiveChecker, OpenidConfigurationGetter, ReadyChecker } from './misc/ind import weaviateV2 from './v2/index.js'; import { ConsistencyLevel } from './data/replication.js'; +import users, { Users } from './users/index.js'; export type ProtocolParams = { /** @@ -105,6 +106,7 @@ export interface WeaviateClient { collections: Collections; oidcAuth?: OidcAuthenticator; roles: Roles; + users: Users; close: () => Promise; getMeta: () => Promise; @@ -224,6 +226,7 @@ async function client(params: ClientParams): Promise { cluster: cluster(connection), collections: collections(connection, dbVersionSupport), roles: roles(connection), + users: users(connection), close: () => Promise.resolve(connection.close()), // hedge against future changes to add I/O to .close() getMeta: () => new MetaGetter(connection).do(), getOpenIDConfig: () => new OpenidConfigurationGetter(connection.http).do(), diff --git a/src/openapi/types.ts b/src/openapi/types.ts index 44b7d0d8..304dd1d5 100644 --- a/src/openapi/types.ts +++ b/src/openapi/types.ts @@ -54,6 +54,7 @@ export type WeaviateMultiTenancyConfig = WeaviateClass['multiTenancyConfig']; export type WeaviateReplicationConfig = WeaviateClass['replicationConfig']; export type WeaviateShardingConfig = WeaviateClass['shardingConfig']; export type WeaviateShardStatus = definitions['ShardStatusGetResponse']; +export type WeaviateUser = definitions['UserInfo']; export type WeaviateVectorIndexConfig = WeaviateClass['vectorIndexConfig']; export type WeaviateVectorsConfig = WeaviateClass['vectorConfig']; export type WeaviateVectorConfig = definitions['VectorConfig']; diff --git a/src/roles/index.ts b/src/roles/index.ts index 0bf17846..84c1b31c 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -10,37 +10,86 @@ import { PermissionsInput, Role, RolesPermission, - User, } from './types.js'; import { Map } from './util.js'; export interface Roles { + /** + * Retrieve all the roles in the system. + * + * @returns {Promise>} A map of role names to their respective roles. + */ listAll: () => Promise>; - ofCurrentUser: () => Promise>; + /** + * Retrieve a role by its name. + * + * @param {string} roleName The name of the role to retrieve. + * @returns {Promise} The role if it exists, or null if it does not. + */ byName: (roleName: string) => Promise; - byUser: (user: string) => Promise>; - assignedUsers: (roleName: string) => Promise>; + /** + * Retrieve the user IDs assigned to a role. + * + * @param {string} roleName The name of the role to retrieve the assigned user IDs for. + * @returns {Promise} The user IDs assigned to the role. + */ + assignedUserIds: (roleName: string) => Promise; + /** + * Delete a role by its name. + * + * @param {string} roleName The name of the role to delete. + * @returns {Promise} A promise that resolves when the role is deleted. + */ delete: (roleName: string) => Promise; + /** + * Create a new role. + * + * @param {string} roleName The name of the new role. + * @param {PermissionsInput} permissions The permissions to assign to the new role. + * @returns {Promise} The newly created role. + */ create: (roleName: string, permissions: PermissionsInput) => Promise; - assignToUser: (roleNames: string | string[], user: string) => Promise; + /** + * Check if a role exists. + * + * @param {string} roleName The name of the role to check for. + * @returns {Promise} A promise that resolves to true if the role exists, or false if it does not. + */ exists: (roleName: string) => Promise; - revokeFromUser: (roleNames: string | string[], user: string) => Promise; + /** + * Add permissions to a role. + * + * @param {string} roleName The name of the role to add permissions to. + * @param {PermissionsInput} permissions The permissions to add. + * @returns {Promise} A promise that resolves when the permissions are added. + */ addPermissions: (roleName: string, permissions: PermissionsInput) => Promise; + /** + * Remove permissions from a role. + * + * @param {string} roleName The name of the role to remove permissions from. + * @param {PermissionsInput} permissions The permissions to remove. + * @returns {Promise} A promise that resolves when the permissions are removed. + */ removePermissions: (roleName: string, permissions: PermissionsInput) => Promise; - hasPermission: (roleName: string, permission: Permission) => Promise; + /** + * Check if a role has the specified permissions. + * + * @param {string} roleName The name of the role to check. + * @param {Permission | Permission[]} permission The permission or permissions to check for. + * @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; } const roles = (connection: ConnectionREST): Roles => { return { listAll: () => connection.get('/authz/roles').then(Map.roles), - ofCurrentUser: () => connection.get('/authz/users/own-roles').then(Map.roles), byName: (roleName: string) => connection.get(`/authz/roles/${roleName}`).then(Map.roleFromWeaviate), - byUser: (user: string) => connection.get(`/authz/users/${user}/roles`).then(Map.roles), - assignedUsers: (roleName: string) => - connection.get(`/authz/roles/${roleName}/users`).then(Map.users), + assignedUserIds: (roleName: string) => connection.get(`/authz/roles/${roleName}/users`), create: (roleName: string, permissions: PermissionsInput) => { - const perms = Map.flattenPermissions(permissions).map(Map.permissionToWeaviate); + const perms = Map.flattenPermissions(permissions).flatMap(Map.permissionToWeaviate); return connection .postEmpty('/authz/roles', { name: roleName, @@ -54,23 +103,18 @@ const roles = (connection: ConnectionREST): Roles => { .get(`/authz/roles/${roleName}`) .then(() => true) .catch(() => false), - assignToUser: (roleNames: string | string[], user: string) => - connection.postEmpty(`/authz/users/${user}/assign`, { - roles: Array.isArray(roleNames) ? roleNames : [roleNames], - }), - revokeFromUser: (roleNames: string | string[], user: string) => - connection.postEmpty(`/authz/users/${user}/revoke`, { - roles: Array.isArray(roleNames) ? roleNames : [roleNames], - }), addPermissions: (roleName: string, permissions: PermissionsInput) => connection.postEmpty(`/authz/roles/${roleName}/add-permissions`, { permissions }), removePermissions: (roleName: string, permissions: PermissionsInput) => connection.postEmpty(`/authz/roles/${roleName}/remove-permissions`, { permissions }), - hasPermission: (roleName: string, permission: Permission) => - connection.postReturn( - `/authz/roles/${roleName}/has-permission`, - Map.permissionToWeaviate(permission) - ), + hasPermissions: (roleName: string, permission: Permission | Permission[]) => + Promise.all( + (Array.isArray(permission) ? permission : [permission]) + .flatMap((p) => Map.permissionToWeaviate(p)) + .map((p) => + connection.postReturn(`/authz/roles/${roleName}/has-permission`, p) + ) + ).then((r) => r.every((b) => b)), }; }; @@ -78,19 +122,15 @@ export const permissions = { backup: (args: { collection: string | string[]; manage?: boolean }): BackupsPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { - const out: BackupsPermission[] = []; - if (args.manage) { - out.push({ collection, action: 'manage_backups' }); - } + const out: BackupsPermission = { collection, actions: [] }; + if (args.manage) out.actions.push('manage_backups'); return out; }); }, cluster: (args: { read?: boolean }): ClusterPermission[] => { - const out: ClusterPermission[] = []; - if (args.read) { - out.push({ action: 'read_cluster' }); - } - return out; + const out: ClusterPermission = { actions: [] }; + if (args.read) out.actions.push('read_cluster'); + return [out]; }, collections: (args: { collection: string | string[]; @@ -101,19 +141,11 @@ export const permissions = { }): CollectionsPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { - const out: CollectionsPermission[] = []; - if (args.create_collection) { - out.push({ collection, action: 'create_collections' }); - } - if (args.read_config) { - out.push({ collection, action: 'read_collections' }); - } - if (args.update_config) { - out.push({ collection, action: 'update_collections' }); - } - if (args.delete_collection) { - out.push({ collection, action: 'delete_collections' }); - } + const out: CollectionsPermission = { collection, actions: [] }; + if (args.create_collection) out.actions.push('create_collections'); + if (args.read_config) out.actions.push('read_collections'); + if (args.update_config) out.actions.push('update_collections'); + if (args.delete_collection) out.actions.push('delete_collections'); return out; }); }, @@ -126,19 +158,11 @@ export const permissions = { }): DataPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { - const out: DataPermission[] = []; - if (args.create) { - out.push({ collection, action: 'create_data' }); - } - if (args.read) { - out.push({ collection, action: 'read_data' }); - } - if (args.update) { - out.push({ collection, action: 'update_data' }); - } - if (args.delete) { - out.push({ collection, action: 'delete_data' }); - } + const out: DataPermission = { collection, actions: [] }; + if (args.create) out.actions.push('create_data'); + if (args.read) out.actions.push('read_data'); + if (args.update) out.actions.push('update_data'); + if (args.delete) out.actions.push('delete_data'); return out; }); }, @@ -149,23 +173,21 @@ export const permissions = { }): NodesPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { - const out: NodesPermission[] = []; - if (args.read) { - out.push({ collection, action: 'read_nodes', verbosity: args.verbosity || 'verbose' }); - } + const out: NodesPermission = { + collection, + actions: [], + verbosity: args.verbosity || 'verbose', + }; + if (args.read) out.actions.push('read_nodes'); return out; }); }, roles: (args: { role: string | string[]; read?: boolean; manage?: boolean }): RolesPermission[] => { const roles = Array.isArray(args.role) ? args.role : [args.role]; return roles.flatMap((role) => { - const out: RolesPermission[] = []; - if (args.read) { - out.push({ role, action: 'read_roles' }); - } - if (args.manage) { - out.push({ role, action: 'manage_roles' }); - } + const out: RolesPermission = { role, actions: [] }; + if (args.read) out.actions.push('read_roles'); + if (args.manage) out.actions.push('manage_roles'); return out; }); }, diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index d65b327b..88a29b7c 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -6,7 +6,7 @@ import { } from '../errors'; import { DbVersion } from '../utils/dbVersion'; -const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 28, 0) +const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) ? describe : describe.skip; @@ -45,11 +45,6 @@ only('Integration testing of the roles namespace', () => { ); }); - it('should get roles by user', async () => { - const roles = await client.roles.byUser('admin-user'); - expect(Object.keys(roles).length).toBeGreaterThan(0); - }); - it('should check the existance of a real role', async () => { const exists = await client.roles.exists('admin'); expect(exists).toBeTruthy(); @@ -72,7 +67,7 @@ only('Integration testing of the roles namespace', () => { permissions: weaviate.permissions.backup({ collection: 'Some-collection', manage: true }), expected: { name: 'backups', - backupsPermissions: [{ collection: 'Some-collection', action: 'manage_backups' }], + backupsPermissions: [{ collection: 'Some-collection', actions: ['manage_backups'] }], clusterPermissions: [], collectionsPermissions: [], dataPermissions: [], @@ -86,7 +81,7 @@ only('Integration testing of the roles namespace', () => { expected: { name: 'cluster', backupsPermissions: [], - clusterPermissions: [{ action: 'read_cluster' }], + clusterPermissions: [{ actions: ['read_cluster'] }], collectionsPermissions: [], dataPermissions: [], nodesPermissions: [], @@ -107,10 +102,10 @@ only('Integration testing of the roles namespace', () => { backupsPermissions: [], clusterPermissions: [], collectionsPermissions: [ - { collection: 'Some-collection', action: 'create_collections' }, - { collection: 'Some-collection', action: 'read_collections' }, - { collection: 'Some-collection', action: 'update_collections' }, - { collection: 'Some-collection', action: 'delete_collections' }, + { + collection: 'Some-collection', + actions: ['create_collections', 'read_collections', 'update_collections', 'delete_collections'], + }, ], dataPermissions: [], nodesPermissions: [], @@ -132,10 +127,10 @@ only('Integration testing of the roles namespace', () => { clusterPermissions: [], collectionsPermissions: [], dataPermissions: [ - { collection: 'Some-collection', action: 'create_data' }, - { collection: 'Some-collection', action: 'read_data' }, - { collection: 'Some-collection', action: 'update_data' }, - { collection: 'Some-collection', action: 'delete_data' }, + { + collection: 'Some-collection', + actions: ['create_data', 'read_data', 'update_data', 'delete_data'], + }, ], nodesPermissions: [], rolesPermissions: [], @@ -154,7 +149,9 @@ only('Integration testing of the roles namespace', () => { clusterPermissions: [], collectionsPermissions: [], dataPermissions: [], - nodesPermissions: [{ collection: 'Some-collection', verbosity: 'verbose', action: 'read_nodes' }], + nodesPermissions: [ + { collection: 'Some-collection', verbosity: 'verbose', actions: ['read_nodes'] }, + ], rolesPermissions: [], }, }, @@ -168,7 +165,7 @@ only('Integration testing of the roles namespace', () => { collectionsPermissions: [], dataPermissions: [], nodesPermissions: [], - rolesPermissions: [{ role: 'some-role', action: 'manage_roles' }], + rolesPermissions: [{ role: 'some-role', actions: ['manage_roles'] }], }, }, ]; @@ -186,4 +183,10 @@ only('Integration testing of the roles namespace', () => { await expect(client.roles.byName('backups')).rejects.toThrowError(WeaviateUnexpectedStatusCodeError); await expect(client.roles.exists('backups')).resolves.toBeFalsy(); }); + + afterAll(() => + Promise.all( + ['backups', 'cluster', 'collections', 'data', 'nodes', 'roles'].map((n) => client.roles.delete(n)) + ) + ); }); diff --git a/src/roles/types.ts b/src/roles/types.ts index bb11d17e..ee1a712a 100644 --- a/src/roles/types.ts +++ b/src/roles/types.ts @@ -19,32 +19,32 @@ export type RolesAction = Extract; export type BackupsPermission = { collection: string; - action: BackupsAction; + actions: BackupsAction[]; }; export type ClusterPermission = { - action: ClusterAction; + actions: ClusterAction[]; }; export type CollectionsPermission = { collection: string; - action: CollectionsAction; + actions: CollectionsAction[]; }; export type DataPermission = { collection: string; - action: DataAction; + actions: DataAction[]; }; export type NodesPermission = { collection: string; verbosity: 'verbose' | 'minimal'; - action: NodesAction; + actions: NodesAction[]; }; export type RolesPermission = { role: string; - action: RolesAction; + actions: RolesAction[]; }; export type Role = { @@ -57,10 +57,6 @@ export type Role = { rolesPermissions: RolesPermission[]; }; -export type User = { - name: string; -}; - export type Permission = | BackupsPermission | ClusterPermission diff --git a/src/roles/util.ts b/src/roles/util.ts index 299579f5..9447dae0 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -1,4 +1,5 @@ -import { Permission as WeaviatePermission, Role as WeaviateRole } from '../openapi/types.js'; +import { Permission as WeaviatePermission, Role as WeaviateRole, WeaviateUser } from '../openapi/types.js'; +import { User } from '../users/types.js'; import { BackupsAction, BackupsPermission, @@ -14,30 +15,37 @@ import { Role, RolesAction, RolesPermission, - User, } from './types.js'; export class PermissionGuards { + private static includes = (permission: Permission, ...actions: string[]): boolean => + actions.filter((a) => Array.from(permission.actions).includes(a)).length > 0; static isBackups = (permission: Permission): permission is BackupsPermission => - (permission as BackupsPermission).action === 'manage_backups'; + PermissionGuards.includes(permission, 'manage_backups'); static isCluster = (permission: Permission): permission is ClusterPermission => - (permission as ClusterPermission).action === 'read_cluster'; + PermissionGuards.includes(permission, 'read_cluster'); static isCollections = (permission: Permission): permission is CollectionsPermission => - [ + PermissionGuards.includes( + permission, 'create_collections', 'delete_collections', 'read_collections', 'update_collections', - 'manage_collections', - ].includes((permission as CollectionsPermission).action); + 'manage_collections' + ); static isData = (permission: Permission): permission is DataPermission => - ['create_data', 'delete_data', 'read_data', 'update_data', 'manage_data'].includes( - (permission as DataPermission).action + PermissionGuards.includes( + permission, + 'create_data', + 'delete_data', + 'read_data', + 'update_data', + 'manage_data' ); static isNodes = (permission: Permission): permission is NodesPermission => - (permission as NodesPermission).action === 'read_nodes'; + PermissionGuards.includes(permission, 'read_nodes'); static isRoles = (permission: Permission): permission is RolesPermission => - (permission as RolesPermission).action === 'manage_roles'; + PermissionGuards.includes(permission, 'manage_roles'); static isPermission = (permissions: PermissionsInput): permissions is Permission => !Array.isArray(permissions); static isPermissionArray = (permissions: PermissionsInput): permissions is Permission[] => @@ -56,89 +64,87 @@ export class Map { static flattenPermissions = (permissions: PermissionsInput): Permission[] => !Array.isArray(permissions) ? [permissions] : permissions.flat(2); - static permissionToWeaviate = (permission: Permission): WeaviatePermission => { + static permissionToWeaviate = (permission: Permission): WeaviatePermission[] => { if (PermissionGuards.isBackups(permission)) { - return { backups: { collection: permission.collection }, action: permission.action }; + return Array.from(permission.actions).map((action) => ({ + backups: { collection: permission.collection }, + action, + })); } else if (PermissionGuards.isCluster(permission)) { - return { action: permission.action }; + return Array.from(permission.actions).map((action) => ({ action })); } else if (PermissionGuards.isCollections(permission)) { - return { collections: { collection: permission.collection }, action: permission.action }; + return Array.from(permission.actions).map((action) => ({ + collections: { collection: permission.collection }, + action, + })); } else if (PermissionGuards.isData(permission)) { - return { data: { collection: permission.collection }, action: permission.action }; + return Array.from(permission.actions).map((action) => ({ + data: { collection: permission.collection }, + action, + })); } else if (PermissionGuards.isNodes(permission)) { - return { + return Array.from(permission.actions).map((action) => ({ nodes: { collection: permission.collection, verbosity: permission.verbosity }, - action: permission.action, - }; + action, + })); } else if (PermissionGuards.isRoles(permission)) { - return { roles: { role: permission.role }, action: permission.action }; + return Array.from(permission.actions).map((action) => ({ roles: { role: permission.role }, action })); } else { throw new Error(`Unknown permission type: ${permission}`); } }; static roleFromWeaviate = (role: WeaviateRole): Role => { - const out: Role = { - name: role.name, - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], + const perms = { + backups: {} as Record, + cluster: {} as Record, + collections: {} as Record, + data: {} as Record, + nodes: {} as Record, + roles: {} as Record, }; role.permissions.forEach((permission) => { if (permission.backups !== undefined) { - if (permission.backups.collection === undefined) { - throw new Error('Backups permission missing collection'); - } - out.backupsPermissions.push({ - collection: permission.backups?.collection, - action: permission.action as BackupsAction, - }); + const key = permission.backups.collection; + if (key === undefined) throw new Error('Backups permission missing collection'); + if (perms.backups[key] === undefined) perms.backups[key] = { collection: key, actions: [] }; + perms.backups[key].actions.push(permission.action as BackupsAction); } else if (permission.action === 'read_cluster') { - out.clusterPermissions.push({ - action: permission.action, - }); + if (perms.cluster[''] === undefined) perms.cluster[''] = { actions: [] }; + perms.cluster[''].actions.push('read_cluster'); } else if (permission.collections !== undefined) { - if (permission.collections.collection === undefined) { - throw new Error('Collections permission missing collection'); - } - out.collectionsPermissions.push({ - collection: permission.collections.collection, - action: permission.action as CollectionsAction, - }); + const key = permission.collections.collection; + if (key === undefined) throw new Error('Collections permission missing collection'); + if (perms.collections[key] === undefined) perms.collections[key] = { collection: key, actions: [] }; + perms.collections[key].actions.push(permission.action as CollectionsAction); } else if (permission.data !== undefined) { - if (permission.data.collection === undefined) { - throw new Error('Data permission missing collection'); - } - out.dataPermissions.push({ - collection: permission.data.collection, - action: permission.action as DataAction, - }); + const key = permission.data.collection; + if (key === undefined) throw new Error('Data permission missing collection'); + if (perms.data[key] === undefined) perms.data[key] = { collection: key, actions: [] }; + perms.data[key].actions.push(permission.action as DataAction); } else if (permission.nodes !== undefined) { - if (permission.nodes.collection === undefined) { - throw new Error('Nodes permission missing collection'); - } - if (permission.nodes.verbosity === undefined) { - throw new Error('Nodes permission missing verbosity'); - } - out.nodesPermissions.push({ - collection: permission.nodes.collection, - verbosity: permission.nodes.verbosity, - action: permission.action as NodesAction, - }); + const { collection, verbosity } = permission.nodes; + if (collection === undefined) throw new Error('Nodes permission missing collection'); + if (verbosity === undefined) throw new Error('Nodes permission missing verbosity'); + const key = `${collection}#${verbosity}`; + if (perms.nodes[key] === undefined) perms.nodes[key] = { collection, verbosity, actions: [] }; + perms.nodes[key].actions.push(permission.action as NodesAction); } else if (permission.roles !== undefined) { - if (permission.roles.role === undefined) { - throw new Error('Roles permission missing role'); - } - out.rolesPermissions.push({ - role: permission.roles.role, - action: permission.action as RolesAction, - }); + const key = permission.roles.role; + if (key === undefined) throw new Error('Roles permission missing role'); + if (perms.roles[key] === undefined) perms.roles[key] = { role: key, actions: [] }; + perms.roles[key].actions.push(permission.action as RolesAction); } }); - return out; + return { + name: role.name, + backupsPermissions: Object.values(perms.backups), + clusterPermissions: Object.values(perms.cluster), + collectionsPermissions: Object.values(perms.collections), + dataPermissions: Object.values(perms.data), + nodesPermissions: Object.values(perms.nodes), + rolesPermissions: Object.values(perms.roles), + }; }; static roles = (roles: WeaviateRole[]): Record => @@ -149,7 +155,11 @@ export class Map { static users = (users: string[]): Record => users.reduce((acc, user) => { - acc[user] = { name: user }; + acc[user] = { id: user }; return acc; }, {} as Record); + static user = (user: WeaviateUser): User => ({ + id: user.username, + roles: user.roles?.map(Map.roleFromWeaviate), + }); } diff --git a/src/users/index.ts b/src/users/index.ts new file mode 100644 index 00000000..353909fc --- /dev/null +++ b/src/users/index.ts @@ -0,0 +1,55 @@ +import { ConnectionREST } from '../index.js'; +import { Role as WeaviateRole, WeaviateUser } from '../openapi/types.js'; +import { Role } from '../roles/types.js'; +import { Map } from '../roles/util.js'; +import { User } from './types.js'; + +export interface Users { + /** + * Retrieve the information relevant to the currently authenticated user. + * + * @returns {Promise} The user information. + */ + getMyUser: () => Promise; + /** + * Retrieve the roles assigned to a user. + * + * @param {string} userId The ID of the user to retrieve the assigned roles for. + * @returns {Promise>} A map of role names to their respective roles. + */ + getAssignedRoles: (userId: string) => Promise>; + /** + * Assign roles to a user. + * + * @param {string | string[]} roleNames The name or names of the roles to assign. + * @param {string} userId The ID of the user to assign the roles to. + * @returns {Promise} A promise that resolves when the roles are assigned. + */ + assignRoles: (roleNames: string | string[], userId: string) => Promise; + /** + * Revoke roles from a user. + * + * @param {string | string[]} roleNames The name or names of the roles to revoke. + * @param {string} userId The ID of the user to revoke the roles from. + * @returns {Promise} A promise that resolves when the roles are revoked. + */ + revokeRoles: (roleNames: string | string[], userId: string) => Promise; +} + +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), + assignRoles: (roleNames: string | string[], userId: string) => + connection.postEmpty(`/authz/users/${userId}/assign`, { + roles: Array.isArray(roleNames) ? roleNames : [roleNames], + }), + revokeRoles: (roleNames: string | string[], userId: string) => + connection.postEmpty(`/authz/users/${userId}/revoke`, { + roles: Array.isArray(roleNames) ? roleNames : [roleNames], + }), + }; +}; + +export default users; diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts new file mode 100644 index 00000000..83d2ec4a --- /dev/null +++ b/src/users/integration.test.ts @@ -0,0 +1,63 @@ +import weaviate, { ApiKey } from '..'; +import { DbVersion } from '../utils/dbVersion'; + +const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) + ? describe + : describe.skip; + +only('Integration testing of the users namespace', () => { + const makeClient = (key: string) => + weaviate.connectToLocal({ + port: 8091, + grpcPort: 50062, + authCredentials: new ApiKey(key), + }); + + beforeAll(() => + makeClient('admin-key').then((c) => + c.roles.create('test', weaviate.permissions.data({ collection: 'Thing', read: true })) + ) + ); + + it('should be able to retrieve own admin user with root roles', async () => { + const user = await makeClient('admin-key').then((client) => client.users.getMyUser()); + expect(user.id).toBe('admin-user'); // defined in the compose file in the ci/ dir + expect(user.roles).toBeDefined(); + }); + + it('should be able to retrieve own custom user with no roles', async () => { + const user = await makeClient('custom-key').then((client) => client.users.getMyUser()); + expect(user.id).toBe('custom-user'); // defined in the compose file in the ci/ dir + expect(user.roles).toBeUndefined(); + }); + + it('should be able to retrieve the assigned roles of a user', async () => { + const roles = await makeClient('admin-key').then((client) => client.users.getAssignedRoles('admin-user')); + expect(roles.root).toBeDefined(); + expect(roles.root.backupsPermissions.length).toBeGreaterThan(0); + expect(roles.root.clusterPermissions.length).toBeGreaterThan(0); + expect(roles.root.collectionsPermissions.length).toBeGreaterThan(0); + expect(roles.root.dataPermissions.length).toBeGreaterThan(0); + expect(roles.root.nodesPermissions.length).toBeGreaterThan(0); + expect(roles.root.rolesPermissions.length).toBeGreaterThan(0); + }); + + it('should be able to assign a role to a user', async () => { + const adminClient = await makeClient('admin-key'); + await adminClient.users.assignRoles('test', 'custom-user'); + + const roles = await adminClient.users.getAssignedRoles('custom-user'); + expect(roles.test).toBeDefined(); + expect(roles.test.dataPermissions.length).toEqual(1); + }); + + it('should be able to revoke a role from a user', async () => { + const adminClient = await makeClient('admin-key'); + await adminClient.users.revokeRoles('test', 'custom-user'); + + const roles = await adminClient.users.getAssignedRoles('custom-user'); + expect(roles.test).toBeUndefined(); + }); + + afterAll(() => makeClient('admin-key').then((c) => c.roles.delete('test'))); +}); diff --git a/src/users/types.ts b/src/users/types.ts new file mode 100644 index 00000000..097b1c57 --- /dev/null +++ b/src/users/types.ts @@ -0,0 +1,6 @@ +import { Role } from '../roles/types.js'; + +export type User = { + id: string; + roles?: Role[]; +};