diff --git a/src/roles/index.ts b/src/roles/index.ts index 2fbe723b..cef48b68 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -121,6 +121,15 @@ const roles = (connection: ConnectionREST): Roles => { }; export const permissions = { + /** + * Create a set of permissions specific to Weaviate's backup functionality. + * + * For all collections, provide the `collection` argument as `'*'`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {boolean} [args.manage] Whether to allow managing backups. Defaults to `false`. + * @returns {BackupsPermission[]} The permissions for the specified collections. + */ backup: (args: { collection: string | string[]; manage?: boolean }): BackupsPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { @@ -129,11 +138,27 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to Weaviate's cluster endpoints. + * + * @param {boolean} [args.read] Whether to allow reading cluster information. Defaults to `false`. + */ cluster: (args: { read?: boolean }): ClusterPermission[] => { const out: ClusterPermission = { actions: [] }; if (args.read) out.actions.push('read_cluster'); return [out]; }, + /** + * Create a set of permissions specific to any operations involving collections. + * + * For all collections, provide the `collection` argument as `'*'`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {boolean} [args.create_collection] Whether to allow creating collections. Defaults to `false`. + * @param {boolean} [args.read_config] Whether to allow reading collection configurations. Defaults to `false`. + * @param {boolean} [args.update_config] Whether to allow updating collection configurations. Defaults to `false`. + * @param {boolean} [args.delete_collection] Whether to allow deleting collections. Defaults to `false`. + */ collections: (args: { collection: string | string[]; create_collection?: boolean; @@ -151,16 +176,37 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to any operations involving objects within collections and tenants. + * + * For all collections, provide the `collection` argument as `'*'`. + * For all tenants, provide the `tenant` argument as `'*'`. + * + * Providing arrays of collections and tenants will create permissions for each combination of collection and tenant. + * E.g., `data({ collection: ['A', 'B'], tenant: ['X', 'Y'] })` will create permissions for tenants `X` and `Y` in both collections `A` and `B`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {string | string[]} [args.tenant] The tenant or tenants to create permissions for. Defaults to `'*'`. + * @param {boolean} [args.create] Whether to allow creating objects. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading objects. Defaults to `false`. + * @param {boolean} [args.update] Whether to allow updating objects. Defaults to `false`. + * @param {boolean} [args.delete] Whether to allow deleting objects. Defaults to `false`. + */ data: (args: { collection: string | string[]; + tenant?: string | string[]; create?: boolean; read?: boolean; update?: boolean; delete?: boolean; }): DataPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; - return collections.flatMap((collection) => { - const out: DataPermission = { collection, actions: [] }; + const tenants = Array.isArray(args.tenant) ? args.tenant : [args.tenant ?? '*']; + const combinations = collections.flatMap((collection) => + tenants.map((tenant) => ({ collection, tenant })) + ); + return combinations.flatMap(({ collection, tenant }) => { + const out: DataPermission = { collection, tenant, 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'); @@ -168,7 +214,16 @@ export const permissions = { return out; }); }, + /** + * This namespace contains methods to create permissions specific to nodes. + */ nodes: { + /** + * Create a set of permissions specific to reading nodes with verbosity set to `minimal`. + * + * @param {boolean} [args.read] Whether to allow reading nodes. Defaults to `false`. + * @returns {NodesPermission[]} The permissions for reading nodes. + */ minimal: (args: { read?: boolean }): NodesPermission[] => { const out: NodesPermission = { collection: '*', @@ -178,6 +233,13 @@ export const permissions = { if (args.read) out.actions.push('read_nodes'); return [out]; }, + /** + * Create a set of permissions specific to reading nodes with verbosity set to `verbose`. + * + * @param {string | string[]} args.collection The collection or collections to create permissions for. + * @param {boolean} [args.read] Whether to allow reading nodes. Defaults to `false`. + * @returns {NodesPermission[]} The permissions for reading nodes. + */ verbose: (args: { collection: string | string[]; read?: boolean }): NodesPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; return collections.flatMap((collection) => { @@ -191,6 +253,16 @@ export const permissions = { }); }, }, + /** + * Create a set of permissions specific to any operations involving roles. + * + * @param {string | string[]} args.role The role or roles to create permissions for. + * @param {boolean} [args.create] Whether to allow creating roles. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading roles. Defaults to `false`. + * @param {boolean} [args.update] Whether to allow updating roles. Defaults to `false`. + * @param {boolean} [args.delete] Whether to allow deleting roles. Defaults to `false`. + * @returns {RolesPermission[]} The permissions for the specified roles. + */ roles: (args: { role: string | string[]; create?: boolean; @@ -208,16 +280,38 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to any operations involving tenants. + * + * For all collections, provide the `collection` argument as `'*'`. + * For all tenants, provide the `tenant` argument as `'*'`. + * + * Providing arrays of collections and tenants will create permissions for each combination of collection and tenant. + * E.g., `tenants({ collection: ['A', 'B'], tenant: ['X', 'Y'] })` will create permissions for tenants `X` and `Y` in both collections `A` and `B`. + * + * @param {string | string[] | Record} args.collection The collection or collections to create permissions for. + * @param {string | string[]} [args.tenant] The tenant or tenants to create permissions for. Defaults to `'*'`. + * @param {boolean} [args.create] Whether to allow creating tenants. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading tenants. Defaults to `false`. + * @param {boolean} [args.update] Whether to allow updating tenants. Defaults to `false`. + * @param {boolean} [args.delete] Whether to allow deleting tenants. Defaults to `false`. + * @returns {TenantsPermission[]} The permissions for the specified tenants. + */ tenants: (args: { collection: string | string[]; + tenant?: string | string[]; create?: boolean; read?: boolean; update?: boolean; delete?: boolean; }): TenantsPermission[] => { const collections = Array.isArray(args.collection) ? args.collection : [args.collection]; - return collections.flatMap((collection) => { - const out: TenantsPermission = { collection, actions: [] }; + const tenants = Array.isArray(args.tenant) ? args.tenant : [args.tenant ?? '*']; + const combinations = collections.flatMap((collection) => + tenants.map((tenant) => ({ collection, tenant })) + ); + return combinations.flatMap(({ collection, tenant }) => { + const out: TenantsPermission = { collection, tenant, actions: [] }; if (args.create) out.actions.push('create_tenants'); if (args.read) out.actions.push('read_tenants'); if (args.update) out.actions.push('update_tenants'); @@ -225,15 +319,23 @@ export const permissions = { return out; }); }, + /** + * Create a set of permissions specific to any operations involving users. + * + * @param {string | string[]} args.user The user or users to create permissions for. + * @param {boolean} [args.assignAndRevoke] Whether to allow assigning and revoking users. Defaults to `false`. + * @param {boolean} [args.read] Whether to allow reading users. Defaults to `false`. + * @returns {UsersPermission[]} The permissions for the specified users. + */ users: (args: { user: string | string[]; - assign_and_revoke?: boolean; + assignAndRevoke?: 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.assignAndRevoke) out.actions.push('assign_and_revoke_users'); if (args.read) out.actions.push('read_users'); return out; }); diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts index 7d083209..2963276f 100644 --- a/src/roles/integration.test.ts +++ b/src/roles/integration.test.ts @@ -1,12 +1,288 @@ -import weaviate, { ApiKey, Permission, Role, WeaviateClient } from '..'; +import weaviate, { + ApiKey, + CollectionsAction, + DataAction, + Permission, + Role, + RolesAction, + TenantsAction, + WeaviateClient, +} from '..'; import { WeaviateStartUpError, WeaviateUnexpectedStatusCodeError } from '../errors'; import { DbVersion } from '../utils/dbVersion'; -const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) +type TestCase = { + roleName: string; + permissions: Permission[]; + expected: Role; +}; + +const emptyPermissions = { + backupsPermissions: [], + clusterPermissions: [], + collectionsPermissions: [], + dataPermissions: [], + nodesPermissions: [], + rolesPermissions: [], + tenantsPermissions: [], + usersPermissions: [], +}; +const crud = { + create: true, + read: true, + update: true, + delete: true, +}; +const collectionsActions: CollectionsAction[] = [ + 'create_collections', + 'read_collections', + 'update_collections', + 'delete_collections', +]; +const dataActions: DataAction[] = ['create_data', 'read_data', 'update_data', 'delete_data']; +const tenantsActions: TenantsAction[] = [ + 'create_tenants', + 'read_tenants', + 'update_tenants', + 'delete_tenants', +]; +const rolesActions: RolesAction[] = ['create_roles', 'read_roles', 'update_roles', 'delete_roles']; +const testCases: TestCase[] = [ + { + roleName: 'backups', + permissions: weaviate.permissions.backup({ collection: 'Some-collection', manage: true }), + expected: { + name: 'backups', + ...emptyPermissions, + backupsPermissions: [{ collection: 'Some-collection', actions: ['manage_backups'] }], + }, + }, + { + roleName: 'cluster', + permissions: weaviate.permissions.cluster({ read: true }), + expected: { + name: 'cluster', + ...emptyPermissions, + clusterPermissions: [{ actions: ['read_cluster'] }], + }, + }, + { + roleName: 'collections', + permissions: weaviate.permissions.collections({ + collection: 'Some-collection', + create_collection: true, + read_config: true, + update_config: true, + delete_collection: true, + }), + expected: { + name: 'collections', + ...emptyPermissions, + collectionsPermissions: [ + { + collection: 'Some-collection', + actions: collectionsActions, + }, + ], + }, + }, + { + roleName: 'data-st', + permissions: weaviate.permissions.data({ + collection: 'Some-collection', + ...crud, + }), + expected: { + name: 'data-st', + ...emptyPermissions, + dataPermissions: [ + { + collection: 'Some-collection', + tenant: '*', + actions: dataActions, + }, + ], + }, + }, + { + roleName: 'data-mt', + permissions: weaviate.permissions.data({ + collection: 'Some-collection', + tenant: 'some-tenant', + ...crud, + }), + expected: { + name: 'data-mt', + ...emptyPermissions, + dataPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: dataActions, + }, + ], + }, + }, + { + roleName: 'data-mt-mixed', + permissions: weaviate.permissions.data({ + collection: ['Some-collection', 'Another-collection'], + tenant: ['some-tenant', 'another-tenant'], + ...crud, + }), + expected: { + name: 'data-mt-mixed', + ...emptyPermissions, + dataPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: dataActions, + }, + { + collection: 'Some-collection', + tenant: 'another-tenant', + actions: dataActions, + }, + { + collection: 'Another-collection', + tenant: 'some-tenant', + actions: dataActions, + }, + { + collection: 'Another-collection', + tenant: 'another-tenant', + actions: dataActions, + }, + ], + }, + }, + { + roleName: 'nodes-verbose', + permissions: weaviate.permissions.nodes.verbose({ + collection: 'Some-collection', + read: true, + }), + expected: { + name: 'nodes-verbose', + ...emptyPermissions, + nodesPermissions: [{ collection: 'Some-collection', verbosity: 'verbose', actions: ['read_nodes'] }], + }, + }, + { + roleName: 'nodes-minimal', + permissions: weaviate.permissions.nodes.minimal({ + read: true, + }), + expected: { + name: 'nodes-minimal', + ...emptyPermissions, + nodesPermissions: [{ collection: '*', verbosity: 'minimal', actions: ['read_nodes'] }], + }, + }, + { + roleName: 'roles', + permissions: weaviate.permissions.roles({ + role: 'some-role', + ...crud, + }), + expected: { + name: 'roles', + ...emptyPermissions, + rolesPermissions: [{ role: 'some-role', actions: rolesActions }], + }, + }, + { + roleName: 'tenants-st', + permissions: weaviate.permissions.tenants({ + collection: 'some-collection', + ...crud, + }), + expected: { + name: 'tenants-st', + ...emptyPermissions, + tenantsPermissions: [ + { + collection: 'Some-collection', + tenant: '*', + actions: tenantsActions, + }, + ], + }, + }, + { + roleName: 'tenants-mt', + permissions: weaviate.permissions.tenants({ + collection: 'some-collection', + tenant: 'some-tenant', + ...crud, + }), + expected: { + name: 'tenants-mt', + ...emptyPermissions, + tenantsPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: tenantsActions, + }, + ], + }, + }, + { + roleName: 'tenants-mt-mixed', + permissions: weaviate.permissions.tenants({ + collection: ['some-collection', 'another-collection'], + tenant: ['some-tenant', 'another-tenant'], + ...crud, + }), + expected: { + name: 'tenants-mt-mixed', + ...emptyPermissions, + tenantsPermissions: [ + { + collection: 'Some-collection', + tenant: 'some-tenant', + actions: tenantsActions, + }, + { + collection: 'Some-collection', + tenant: 'another-tenant', + actions: tenantsActions, + }, + { + collection: 'Another-collection', + tenant: 'some-tenant', + actions: tenantsActions, + }, + { + collection: 'Another-collection', + tenant: 'another-tenant', + actions: tenantsActions, + }, + ], + }, + }, + { + roleName: 'users', + permissions: weaviate.permissions.users({ + user: 'some-user', + assignAndRevoke: true, + read: true, + }), + expected: { + name: 'users', + ...emptyPermissions, + usersPermissions: [{ users: 'some-user', actions: ['assign_and_revoke_users', 'read_users'] }], + }, + }, +]; + +const maybe = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 29, 0) ? describe : describe.skip; -only('Integration testing of the roles namespace', () => { +maybe('Integration testing of the roles namespace', () => { let client: WeaviateClient; beforeAll(async () => { @@ -41,200 +317,6 @@ only('Integration testing of the roles namespace', () => { }); describe('should be able to create roles using the permissions factory', () => { - type TestCase = { - roleName: string; - permissions: Permission[]; - expected: Role; - }; - const testCases: TestCase[] = [ - { - roleName: 'backups', - permissions: weaviate.permissions.backup({ collection: 'Some-collection', manage: true }), - expected: { - name: 'backups', - backupsPermissions: [{ collection: 'Some-collection', actions: ['manage_backups'] }], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'cluster', - permissions: weaviate.permissions.cluster({ read: true }), - expected: { - name: 'cluster', - backupsPermissions: [], - clusterPermissions: [{ actions: ['read_cluster'] }], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'collections', - permissions: weaviate.permissions.collections({ - collection: 'Some-collection', - create_collection: true, - read_config: true, - update_config: true, - delete_collection: true, - }), - expected: { - name: 'collections', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [ - { - collection: 'Some-collection', - actions: ['create_collections', 'read_collections', 'update_collections', 'delete_collections'], - }, - ], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'data', - permissions: weaviate.permissions.data({ - collection: 'Some-collection', - create: true, - read: true, - update: true, - delete: true, - }), - expected: { - name: 'data', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [ - { - collection: 'Some-collection', - actions: ['create_data', 'read_data', 'update_data', 'delete_data'], - }, - ], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'nodes-verbose', - permissions: weaviate.permissions.nodes.verbose({ - collection: 'Some-collection', - read: true, - }), - expected: { - name: 'nodes-verbose', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [ - { collection: 'Some-collection', verbosity: 'verbose', actions: ['read_nodes'] }, - ], - rolesPermissions: [], - tenantsPermissions: [], - 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: [], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'roles', - permissions: weaviate.permissions.roles({ - role: 'some-role', - create: true, - read: true, - update: true, - delete: true, - }), - expected: { - name: 'roles', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [ - { role: 'some-role', actions: ['create_roles', 'read_roles', 'update_roles', 'delete_roles'] }, - ], - tenantsPermissions: [], - usersPermissions: [], - }, - }, - { - roleName: 'tenants', - permissions: weaviate.permissions.tenants({ - collection: 'some-collection', - create: true, - read: true, - update: true, - delete: true, - }), - expected: { - name: 'tenants', - backupsPermissions: [], - clusterPermissions: [], - collectionsPermissions: [], - dataPermissions: [], - nodesPermissions: [], - rolesPermissions: [], - tenantsPermissions: [ - { - collection: 'Some-collection', - actions: ['create_tenants', 'read_tenants', 'update_tenants', 'delete_tenants'], - }, - ], - 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: [], - tenantsPermissions: [], - usersPermissions: [{ users: 'some-user', actions: ['assign_and_revoke_users', 'read_users'] }], - }, - }, - ]; testCases.forEach((testCase) => { it(`with ${testCase.roleName} permissions`, async () => { await client.roles.create(testCase.roleName, testCase.permissions); @@ -250,19 +332,5 @@ only('Integration testing of the roles namespace', () => { await expect(client.roles.exists('backups')).resolves.toBeFalsy(); }); - afterAll(() => - Promise.all( - [ - 'backups', - 'cluster', - 'collections', - 'data', - 'nodes-verbose', - 'nodes-minimal', - 'roles', - 'tenants', - 'users', - ].map((n) => client.roles.delete(n)) - ) - ); + afterAll(() => Promise.all(testCases.map((t) => client.roles.delete(t.roleName)))); }); diff --git a/src/roles/types.ts b/src/roles/types.ts index 6f314245..3a6d0266 100644 --- a/src/roles/types.ts +++ b/src/roles/types.ts @@ -38,6 +38,7 @@ export type CollectionsPermission = { export type DataPermission = { collection: string; + tenant: string; actions: DataAction[]; }; @@ -54,6 +55,7 @@ export type RolesPermission = { export type TenantsPermission = { collection: string; + tenant: string; actions: TenantsAction[]; }; diff --git a/src/roles/util.ts b/src/roles/util.ts index b4a8a9bf..df3072ef 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -81,35 +81,35 @@ export class Map { static permissionToWeaviate = (permission: Permission): WeaviatePermission[] => { if (PermissionGuards.isBackups(permission)) { return Array.from(permission.actions).map((action) => ({ - backups: { collection: permission.collection }, + backups: permission, action, })); } else if (PermissionGuards.isCluster(permission)) { return Array.from(permission.actions).map((action) => ({ action })); } else if (PermissionGuards.isCollections(permission)) { return Array.from(permission.actions).map((action) => ({ - collections: { collection: permission.collection }, + collections: permission, action, })); } else if (PermissionGuards.isData(permission)) { return Array.from(permission.actions).map((action) => ({ - data: { collection: permission.collection }, + data: permission, action, })); } else if (PermissionGuards.isNodes(permission)) { return Array.from(permission.actions).map((action) => ({ - nodes: { collection: permission.collection, verbosity: permission.verbosity }, + nodes: permission, action, })); } else if (PermissionGuards.isRoles(permission)) { - return Array.from(permission.actions).map((action) => ({ roles: { role: permission.role }, action })); + return Array.from(permission.actions).map((action) => ({ roles: permission, action })); } else if (PermissionGuards.isTenants(permission)) { return Array.from(permission.actions).map((action) => ({ - tenants: { collection: permission.collection }, + tenants: permission, action, })); } else if (PermissionGuards.isUsers(permission)) { - return Array.from(permission.actions).map((action) => ({ users: { users: permission.users }, action })); + return Array.from(permission.actions).map((action) => ({ users: permission, action })); } else { throw new Error(`Unknown permission type: ${JSON.stringify(permission, null, 2)}`); } @@ -198,9 +198,11 @@ class PermissionsMapping { private data = (permission: WeaviatePermission) => { if (permission.data !== undefined) { - const key = permission.data.collection; - if (key === undefined) throw new Error('Data permission missing collection'); - if (this.mappings.data[key] === undefined) this.mappings.data[key] = { collection: key, actions: [] }; + const { collection, tenant } = permission.data; + if (collection === undefined) throw new Error('Data permission missing collection'); + const key = tenant === undefined ? collection : `${collection}#${tenant}`; + if (this.mappings.data[key] === undefined) + this.mappings.data[key] = { collection, tenant: tenant || '*', actions: [] }; this.mappings.data[key].actions.push(permission.action as DataAction); } }; @@ -232,10 +234,11 @@ class PermissionsMapping { private tenants = (permission: WeaviatePermission) => { if (permission.tenants !== undefined) { - const key = permission.tenants.collection; - if (key === undefined) throw new Error('Tenants permission missing collection'); + const { collection, tenant } = permission.tenants; + if (collection === undefined) throw new Error('Tenants permission missing collection'); + const key = tenant === undefined ? collection : `${collection}#${tenant}`; if (this.mappings.tenants[key] === undefined) - this.mappings.tenants[key] = { collection: key, actions: [] }; + this.mappings.tenants[key] = { collection, tenant: tenant || '*', actions: [] }; this.mappings.tenants[key].actions.push(permission.action as TenantsAction); } };