diff --git a/src/backup/backupRestorer.ts b/src/backup/backupRestorer.ts index e2f76c0f..011f3e75 100644 --- a/src/backup/backupRestorer.ts +++ b/src/backup/backupRestorer.ts @@ -26,6 +26,7 @@ export default class BackupRestorer extends CommandBase { private statusGetter: BackupRestoreStatusGetter; private waitForCompletion?: boolean; private config?: RestoreConfig; + private overwriteAlias?: boolean; constructor(client: Connection, statusGetter: BackupRestoreStatusGetter) { super(client); @@ -65,6 +66,11 @@ export default class BackupRestorer extends CommandBase { return this; } + withOverwriteAlias(overwriteAlias: boolean) { + this.overwriteAlias = overwriteAlias; + return this; + } + withConfig(cfg: RestoreConfig) { this.config = cfg; return this; @@ -89,6 +95,7 @@ export default class BackupRestorer extends CommandBase { config: this.config, include: this.includeClassNames, exclude: this.excludeClassNames, + overwriteAlias: this.overwriteAlias, } as BackupRestoreRequest; if (this.waitForCompletion) { diff --git a/src/collections/backup/client.ts b/src/collections/backup/client.ts index 6557f070..f220ba0c 100644 --- a/src/collections/backup/client.ts +++ b/src/collections/backup/client.ts @@ -163,6 +163,9 @@ export const backup = (connection: Connection): Backup => { if (args.excludeCollections) { builder = builder.withExcludeClassNames(...args.excludeCollections); } + if (args.config?.overwriteAlias) { + builder = builder.withOverwriteAlias(args.config?.overwriteAlias); + } if (args.config) { builder = builder.withConfig({ CPUPercentage: args.config.cpuPercentage, diff --git a/src/collections/backup/collection.ts b/src/collections/backup/collection.ts index 89ffd864..377d8009 100644 --- a/src/collections/backup/collection.ts +++ b/src/collections/backup/collection.ts @@ -1,6 +1,5 @@ import { Backend } from '../../backup/index.js'; import Connection from '../../connection/index.js'; -import { WeaviateInvalidInputError } from '../../errors.js'; import { backup } from './client.js'; import { BackupReturn, BackupStatusArgs, BackupStatusReturn } from './types.js'; diff --git a/src/collections/backup/integration.test.ts b/src/collections/backup/integration.test.ts index 4cbd4554..cfccb2cf 100644 --- a/src/collections/backup/integration.test.ts +++ b/src/collections/backup/integration.test.ts @@ -3,6 +3,7 @@ /* eslint-disable no-await-in-loop */ import { requireAtLeast } from '../../../test/version.js'; import { Backend } from '../../backup/index.js'; +import { WeaviateBackupFailed } from '../../errors.js'; import weaviate, { Collection, WeaviateClient } from '../../index.js'; // These must run sequentially because Weaviate is not capable of running multiple backups at the same time @@ -117,6 +118,87 @@ describe('Integration testing of backups', () => { .then(testCollectionWaitForCompletion) .then(testCollectionNoWaitForCompletion)); + requireAtLeast(1, 32, 3).describe('overwrite alias', () => { + test('overwriteAlias=true', async () => { + const client = await clientPromise; + + const things = await client.collections.create({ name: 'ThingsTrue' }); + await client.alias.create({ collection: things.name, alias: `${things.name}Alias` }); + + const backup = await client.backup.create({ + backend: 'filesystem', + backupId: randomBackupId(), + includeCollections: [things.name], + waitForCompletion: true, + }); + + await client.collections.delete(things.name); + await client.alias.delete(`${things.name}Alias`); + + // Change alias to point to a different collection + const inventory = await client.collections.create({ name: 'InventoryTrue' }); + await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` }); + + // Restore backup with overwriteAlias=true + await client.backup.restore({ + backend: 'filesystem', + backupId: backup.id, + includeCollections: [things.name], + waitForCompletion: true, + config: { overwriteAlias: true }, + }); + + // Assert: alias points to the original collection + const alias = await client.alias.get(`${things.name}Alias`); + expect(alias.collection).toEqual(things.name); + }); + + test('overwriteAlias=false', async () => { + const client = await clientPromise; + + const things = await client.collections.create({ name: 'ThingsFalse' }); + await client.alias.create({ collection: things.name, alias: `${things.name}Alias` }); + + const backup = await client.backup.create({ + backend: 'filesystem', + backupId: randomBackupId(), + includeCollections: [things.name], + waitForCompletion: true, + }); + + await client.collections.delete(things.name); + await client.alias.delete(`${things.name}Alias`); + + // Change alias to point to a different collection + const inventory = await client.collections.create({ name: 'InventoryFalse' }); + await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` }); + + // Restore backup with overwriteAlias=true + const restored = client.backup.restore({ + backend: 'filesystem', + backupId: backup.id, + includeCollections: [things.name], + waitForCompletion: true, + config: { overwriteAlias: false }, + }); + + // Assert: fails with "alias already exists" error + await expect(restored).rejects.toThrowError(WeaviateBackupFailed); + }); + + it('cleanup', async () => { + await clientPromise.then(async (c) => { + await Promise.all( + ['ThingsTrue', 'ThingsFalse', 'InventoryTrue', 'InventoryFalse'].map((name) => + c.collections.delete(name).catch((e) => {}) + ) + ); + await c.alias.delete('ThingsFalseAlias').catch((e) => {}); + await c.alias.delete('ThingsTrueAlias').catch((e) => {}); + }); + }); + }); + requireAtLeast(1, 32, 0).it('get all exising backups', async () => { await clientPromise.then(async (client) => { await client.collections.create({ name: 'TestListBackups' }).then((col) => col.data.insert()); diff --git a/src/collections/backup/types.ts b/src/collections/backup/types.ts index 250d88e6..4403bb0e 100644 --- a/src/collections/backup/types.ts +++ b/src/collections/backup/types.ts @@ -37,6 +37,8 @@ export type BackupConfigCreate = { export type BackupConfigRestore = { /** The percentage of CPU to use for the backuop restoration job. */ cpuPercentage?: number; + /** Allows ovewriting the collection alias if there is a conflict. */ + overwriteAlias?: boolean; }; /** The arguments required to create and restore backups. */ diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index 9908f40d..409fbfbe 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -110,6 +110,10 @@ export interface paths { '/authz/roles/{id}/user-assignments': { get: operations['getUsersForRole']; }; + '/authz/roles/{id}/group-assignments': { + /** Retrieves a list of all groups that have been assigned a specific role, identified by its name. */ + get: operations['getGroupsForRole']; + }; '/authz/users/{id}/roles': { get: operations['getRolesForUserDeprecated']; }; @@ -128,6 +132,14 @@ export interface paths { '/authz/groups/{id}/revoke': { post: operations['revokeRoleFromGroup']; }; + '/authz/groups/{id}/roles/{groupType}': { + /** Retrieves a list of all roles assigned to a specific group. The group must be identified by both its name (`id`) and its type (`db` or `oidc`). */ + get: operations['getRolesForGroup']; + }; + '/authz/groups/{groupType}': { + /** Retrieves a list of all available group names for a specified group type (`oidc` or `db`). */ + get: operations['getGroups']; + }; '/objects': { /** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */ get: operations['objects.list']; @@ -303,6 +315,11 @@ export interface definitions { * @enum {string} */ UserTypeInput: 'db' | 'oidc'; + /** + * @description If the group contains OIDC or database users. + * @enum {string} + */ + GroupType: 'db' | 'oidc'; /** * @description the type of user * @enum {string} @@ -399,6 +416,15 @@ export interface definitions { */ users?: string; }; + /** @description Resources applicable for group actions. */ + groups?: { + /** + * @description A string that specifies which groups this permission applies to. Can be an exact group name or a regex pattern. The default value `*` applies the permission to all groups. + * @default * + */ + group?: string; + groupType?: definitions['GroupType']; + }; /** @description resources applicable for tenant actions */ tenants?: { /** @@ -496,7 +522,9 @@ export interface definitions { | 'create_aliases' | 'read_aliases' | 'update_aliases' - | 'delete_aliases'; + | 'delete_aliases' + | 'assign_and_revoke_groups' + | 'read_groups'; }; /** @description list of roles */ RolesListResponse: definitions['Role'][]; @@ -1171,8 +1199,6 @@ export interface definitions { BackupListResponse: { /** @description The ID of the backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id?: string; - /** @description destination path of backup files proper to selected backend */ - path?: string; /** @description The list of classes for which the existed backup process */ classes?: string[]; /** @@ -1191,6 +1217,8 @@ export interface definitions { exclude?: string[]; /** @description Allows overriding the node names stored in the backup with different ones. Useful when restoring backups to a different environment. */ node_mapping?: { [key: string]: string }; + /** @description Allows ovewriting the collection alias if there is a conflict */ + overwriteAlias?: boolean; }; /** @description The definition of a backup restore response body */ BackupRestoreResponse: { @@ -1789,7 +1817,9 @@ export interface definitions { | 'WithinGeoRange' | 'IsNull' | 'ContainsAny' - | 'ContainsAll'; + | 'ContainsAll' + | 'ContainsNone' + | 'Not'; /** * @description path to the property currently being filtered * @example [ @@ -2827,6 +2857,42 @@ export interface operations { }; }; }; + /** Retrieves a list of all groups that have been assigned a specific role, identified by its name. */ + getGroupsForRole: { + parameters: { + path: { + /** The unique name of the role. */ + id: string; + }; + }; + responses: { + /** Successfully retrieved the list of groups that have the role assigned. */ + 200: { + schema: ({ + groupId?: string; + groupType: definitions['GroupType']; + } & { + name: unknown; + })[]; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** The specified role was not found. */ + 404: unknown; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; getRolesForUserDeprecated: { parameters: { path: { @@ -2985,6 +3051,7 @@ export interface operations { body: { /** @description the roles that assigned to group */ roles?: string[]; + groupType?: definitions['GroupType']; }; }; }; @@ -3019,6 +3086,7 @@ export interface operations { body: { /** @description the roles that revoked from group */ roles?: string[]; + groupType?: definitions['GroupType']; }; }; }; @@ -3043,6 +3111,80 @@ export interface operations { }; }; }; + /** Retrieves a list of all roles assigned to a specific group. The group must be identified by both its name (`id`) and its type (`db` or `oidc`). */ + getRolesForGroup: { + parameters: { + path: { + /** The unique name of the group. */ + id: string; + /** The type of the group. */ + groupType: 'oidc'; + }; + query: { + /** If true, the response will include the full role definitions with all associated permissions. If false, only role names are returned. */ + includeFullRoles?: boolean; + }; + }; + responses: { + /** A list of roles assigned to the specified group. */ + 200: { + schema: definitions['RolesListResponse']; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** The specified group was not found. */ + 404: unknown; + /** The request syntax is correct, but the server couldn't process it due to semantic issues. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + /** Retrieves a list of all available group names for a specified group type (`oidc` or `db`). */ + getGroups: { + parameters: { + path: { + /** The type of group to retrieve. */ + groupType: 'oidc'; + }; + }; + responses: { + /** A list of group names for the specified type. */ + 200: { + schema: string[]; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** The request syntax is correct, but the server couldn't process it due to semantic issues. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; /** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */ 'objects.list': { parameters: {