Skip to content

Refactor roles namespace, adding users, to align with new Python API #263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 12, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -105,6 +106,7 @@ export interface WeaviateClient {
collections: Collections;
oidcAuth?: OidcAuthenticator;
roles: Roles;
users: Users;

close: () => Promise<void>;
getMeta: () => Promise<Meta>;
Expand Down Expand Up @@ -224,6 +226,7 @@ async function client(params: ClientParams): Promise<WeaviateClient> {
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(),
Expand Down
1 change: 1 addition & 0 deletions src/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
164 changes: 93 additions & 71 deletions src/roles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, Role>>} A map of role names to their respective roles.
*/
listAll: () => Promise<Record<string, Role>>;
ofCurrentUser: () => Promise<Record<string, Role>>;
/**
* Retrieve a role by its name.
*
* @param {string} roleName The name of the role to retrieve.
* @returns {Promise<Role | null>} The role if it exists, or null if it does not.
*/
byName: (roleName: string) => Promise<Role | null>;
byUser: (user: string) => Promise<Record<string, Role>>;
assignedUsers: (roleName: string) => Promise<Record<string, User>>;
/**
* 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<string[]>} The user IDs assigned to the role.
*/
assignedUserIds: (roleName: string) => Promise<string[]>;
/**
* Delete a role by its name.
*
* @param {string} roleName The name of the role to delete.
* @returns {Promise<void>} A promise that resolves when the role is deleted.
*/
delete: (roleName: string) => Promise<void>;
/**
* 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<Role>} The newly created role.
*/
create: (roleName: string, permissions: PermissionsInput) => Promise<Role>;
assignToUser: (roleNames: string | string[], user: string) => Promise<void>;
/**
* Check if a role exists.
*
* @param {string} roleName The name of the role to check for.
* @returns {Promise<boolean>} A promise that resolves to true if the role exists, or false if it does not.
*/
exists: (roleName: string) => Promise<boolean>;
revokeFromUser: (roleNames: string | string[], user: string) => Promise<void>;
/**
* 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<void>} A promise that resolves when the permissions are added.
*/
addPermissions: (roleName: string, permissions: PermissionsInput) => Promise<void>;
/**
* 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<void>} A promise that resolves when the permissions are removed.
*/
removePermissions: (roleName: string, permissions: PermissionsInput) => Promise<void>;
hasPermission: (roleName: string, permission: Permission) => Promise<boolean>;
/**
* 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<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>;
}

const roles = (connection: ConnectionREST): Roles => {
return {
listAll: () => connection.get<WeaviateRole[]>('/authz/roles').then(Map.roles),
ofCurrentUser: () => connection.get<WeaviateRole[]>('/authz/users/own-roles').then(Map.roles),
byName: (roleName: string) =>
connection.get<WeaviateRole>(`/authz/roles/${roleName}`).then(Map.roleFromWeaviate),
byUser: (user: string) => connection.get<WeaviateRole[]>(`/authz/users/${user}/roles`).then(Map.roles),
assignedUsers: (roleName: string) =>
connection.get<string[]>(`/authz/roles/${roleName}/users`).then(Map.users),
assignedUserIds: (roleName: string) => connection.get<string[]>(`/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<WeaviateRole>('/authz/roles', {
name: roleName,
Expand All @@ -54,43 +103,34 @@ 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<WeaviatePermission, boolean>(
`/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<WeaviatePermission, boolean>(`/authz/roles/${roleName}/has-permission`, p)
)
).then((r) => r.every((b) => b)),
};
};

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[];
Expand All @@ -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;
});
},
Expand All @@ -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;
});
},
Expand All @@ -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;
});
},
Expand Down
39 changes: 21 additions & 18 deletions src/roles/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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: [],
Expand All @@ -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: [],
Expand All @@ -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: [],
Expand All @@ -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: [],
Expand All @@ -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: [],
},
},
Expand All @@ -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'] }],
},
},
];
Expand All @@ -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))
)
);
});
Loading