Skip to content

Update roles API to use latest schema and align with recent py changes #266

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 17, 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.1
WEAVIATE_129: 1.29.0-rc.2

jobs:
checks:
Expand Down
1 change: 1 addition & 0 deletions src/openapi/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ export interface definitions {
| 'update_collections'
| 'delete_collections'
| 'assign_and_revoke_users'
| 'read_users'
| 'create_tenants'
| 'read_tenants'
| 'update_tenants'
Expand Down
43 changes: 32 additions & 11 deletions src/roles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PermissionsInput,
Role,
RolesPermission,
UsersPermission,
} from './types.js';
import { Map } from './util.js';

Expand Down Expand Up @@ -166,21 +167,28 @@ export const permissions = {
return out;
});
},
nodes: (args: {
collection: string | string[];
verbosity?: 'verbose' | 'minimal';
read?: boolean;
}): NodesPermission[] => {
const collections = Array.isArray(args.collection) ? args.collection : [args.collection];
return collections.flatMap((collection) => {
nodes: {
minimal: (args: { read?: boolean }): NodesPermission[] => {
const out: NodesPermission = {
collection,
collection: '*',
actions: [],
verbosity: args.verbosity || 'verbose',
verbosity: 'minimal',
};
if (args.read) out.actions.push('read_nodes');
return out;
});
return [out];
},
verbose: (args: { collection: string | string[]; read?: boolean }): NodesPermission[] => {
const collections = Array.isArray(args.collection) ? args.collection : [args.collection];
return collections.flatMap((collection) => {
const out: NodesPermission = {
collection,
actions: [],
verbosity: 'verbose',
};
if (args.read) out.actions.push('read_nodes');
return out;
});
},
},
roles: (args: {
role: string | string[];
Expand All @@ -199,6 +207,19 @@ export const permissions = {
return out;
});
},
users: (args: {
user: string | string[];
assign_and_revoke?: boolean;
read?: boolean;
}): UsersPermission[] => {
const users = Array.isArray(args.user) ? args.user : [args.user];
return users.flatMap((user) => {
const out: UsersPermission = { users: user, actions: [] };
if (args.assign_and_revoke) out.actions.push('assign_and_revoke_users');
if (args.read) out.actions.push('read_users');
return out;
});
},
};

export default roles;
68 changes: 47 additions & 21 deletions src/roles/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import weaviate, { ApiKey, Permission, Role, WeaviateClient } from '..';
import {
WeaviateInsufficientPermissionsError,
WeaviateStartUpError,
WeaviateUnexpectedStatusCodeError,
} from '../errors';
import { WeaviateStartUpError, WeaviateUnexpectedStatusCodeError } from '../errors';
import { DbVersion } from '../utils/dbVersion';

const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0)
Expand Down Expand Up @@ -34,17 +30,6 @@ only('Integration testing of the roles namespace', () => {
})
).rejects.toThrowError(WeaviateStartUpError));

it('should fail with insufficient permissions if permission-less key provided', async () => {
const unauthenticatedClient = await weaviate.connectToLocal({
port: 8091,
grpcPort: 50062,
authCredentials: new ApiKey('custom-key'),
});
await expect(unauthenticatedClient.roles.listAll()).rejects.toThrowError(
WeaviateInsufficientPermissionsError
);
});

it('should check the existance of a real role', async () => {
const exists = await client.roles.exists('admin');
expect(exists).toBeTruthy();
Expand Down Expand Up @@ -73,6 +58,7 @@ only('Integration testing of the roles namespace', () => {
dataPermissions: [],
nodesPermissions: [],
rolesPermissions: [],
usersPermissions: [],
},
},
{
Expand All @@ -86,6 +72,7 @@ only('Integration testing of the roles namespace', () => {
dataPermissions: [],
nodesPermissions: [],
rolesPermissions: [],
usersPermissions: [],
},
},
{
Expand All @@ -110,6 +97,7 @@ only('Integration testing of the roles namespace', () => {
dataPermissions: [],
nodesPermissions: [],
rolesPermissions: [],
usersPermissions: [],
},
},
{
Expand All @@ -134,17 +122,17 @@ only('Integration testing of the roles namespace', () => {
],
nodesPermissions: [],
rolesPermissions: [],
usersPermissions: [],
},
},
{
roleName: 'nodes',
permissions: weaviate.permissions.nodes({
roleName: 'nodes-verbose',
permissions: weaviate.permissions.nodes.verbose({
collection: 'Some-collection',
verbosity: 'verbose',
read: true,
}),
expected: {
name: 'nodes',
name: 'nodes-verbose',
backupsPermissions: [],
clusterPermissions: [],
collectionsPermissions: [],
Expand All @@ -153,6 +141,23 @@ only('Integration testing of the roles namespace', () => {
{ collection: 'Some-collection', verbosity: 'verbose', actions: ['read_nodes'] },
],
rolesPermissions: [],
usersPermissions: [],
},
},
{
roleName: 'nodes-minimal',
permissions: weaviate.permissions.nodes.minimal({
read: true,
}),
expected: {
name: 'nodes-minimal',
backupsPermissions: [],
clusterPermissions: [],
collectionsPermissions: [],
dataPermissions: [],
nodesPermissions: [{ collection: '*', verbosity: 'minimal', actions: ['read_nodes'] }],
rolesPermissions: [],
usersPermissions: [],
},
},
{
Expand All @@ -174,6 +179,25 @@ only('Integration testing of the roles namespace', () => {
rolesPermissions: [
{ role: 'some-role', actions: ['create_roles', 'read_roles', 'update_roles', 'delete_roles'] },
],
usersPermissions: [],
},
},
{
roleName: 'users',
permissions: weaviate.permissions.users({
user: 'some-user',
assign_and_revoke: true,
read: true,
}),
expected: {
name: 'users',
backupsPermissions: [],
clusterPermissions: [],
collectionsPermissions: [],
dataPermissions: [],
nodesPermissions: [],
rolesPermissions: [],
usersPermissions: [{ users: 'some-user', actions: ['assign_and_revoke_users', 'read_users'] }],
},
},
];
Expand All @@ -194,7 +218,9 @@ only('Integration testing of the roles namespace', () => {

afterAll(() =>
Promise.all(
['backups', 'cluster', 'collections', 'data', 'nodes', 'roles'].map((n) => client.roles.delete(n))
['backups', 'cluster', 'collections', 'data', 'nodes-verbose', 'nodes-minimal', 'roles', 'users'].map(
(n) => client.roles.delete(n)
)
)
);
});
10 changes: 9 additions & 1 deletion src/roles/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type DataAction = Extract<
>;
export type NodesAction = Extract<Action, 'read_nodes'>;
export type RolesAction = Extract<Action, 'create_roles' | 'read_roles' | 'update_roles' | 'delete_roles'>;
export type UsersAction = Extract<Action, 'read_users' | 'assign_and_revoke_users'>;

export type BackupsPermission = {
collection: string;
Expand Down Expand Up @@ -47,6 +48,11 @@ export type RolesPermission = {
actions: RolesAction[];
};

export type UsersPermission = {
users: string;
actions: UsersAction[];
};

export type Role = {
name: string;
backupsPermissions: BackupsPermission[];
Expand All @@ -55,6 +61,7 @@ export type Role = {
dataPermissions: DataPermission[];
nodesPermissions: NodesPermission[];
rolesPermissions: RolesPermission[];
usersPermissions: UsersPermission[];
};

export type Permission =
Expand All @@ -63,6 +70,7 @@ export type Permission =
| CollectionsPermission
| DataPermission
| NodesPermission
| RolesPermission;
| RolesPermission
| UsersPermission;

export type PermissionsInput = Permission | Permission[] | Permission[][] | (Permission | Permission[])[];
22 changes: 20 additions & 2 deletions src/roles/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
Role,
RolesAction,
RolesPermission,
UsersAction,
UsersPermission,
} from './types.js';

export class PermissionGuards {
Expand Down Expand Up @@ -46,6 +48,8 @@ export class PermissionGuards {
PermissionGuards.includes(permission, 'read_nodes');
static isRoles = (permission: Permission): permission is RolesPermission =>
PermissionGuards.includes(permission, 'create_role', 'read_roles', 'update_roles', 'delete_roles');
static isUsers = (permission: Permission): permission is UsersPermission =>
PermissionGuards.includes(permission, 'read_users', 'assign_and_revoke_users');
static isPermission = (permissions: PermissionsInput): permissions is Permission =>
!Array.isArray(permissions);
static isPermissionArray = (permissions: PermissionsInput): permissions is Permission[] =>
Expand Down Expand Up @@ -89,6 +93,8 @@ export class Map {
}));
} else if (PermissionGuards.isRoles(permission)) {
return Array.from(permission.actions).map((action) => ({ roles: { role: permission.role }, action }));
} else if (PermissionGuards.isUsers(permission)) {
return Array.from(permission.actions).map((action) => ({ users: { users: permission.users }, action }));
} else {
throw new Error(`Unknown permission type: ${JSON.stringify(permission, null, 2)}`);
}
Expand All @@ -102,6 +108,7 @@ export class Map {
data: {} as Record<string, DataPermission>,
nodes: {} as Record<string, NodesPermission>,
roles: {} as Record<string, RolesPermission>,
users: {} as Record<string, UsersPermission>,
};
role.permissions.forEach((permission) => {
if (permission.backups !== undefined) {
Expand All @@ -123,9 +130,14 @@ export class Map {
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) {
const { collection, verbosity } = permission.nodes;
if (collection === undefined) throw new Error('Nodes permission missing collection');
let { collection } = permission.nodes;
const { verbosity } = permission.nodes;
if (verbosity === undefined) throw new Error('Nodes permission missing verbosity');
if (verbosity === 'verbose') {
if (collection === undefined) throw new Error('Nodes permission missing collection');
} else if (verbosity === 'minimal') collection = '*';
else 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);
Expand All @@ -134,6 +146,11 @@ export class Map {
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);
} else if (permission.users !== undefined) {
const key = permission.users.users;
if (key === undefined) throw new Error('Users permission missing user');
if (perms.users[key] === undefined) perms.users[key] = { users: key, actions: [] };
perms.users[key].actions.push(permission.action as UsersAction);
}
});
return {
Expand All @@ -144,6 +161,7 @@ export class Map {
dataPermissions: Object.values(perms.data),
nodesPermissions: Object.values(perms.nodes),
rolesPermissions: Object.values(perms.roles),
usersPermissions: Object.values(perms.users),
};
};

Expand Down