diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index b9047f28..afa58880 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -8,9 +8,10 @@ on: env: WEAVIATE_124: 1.24.26 - WEAVIATE_125: 1.25.25 - WEAVIATE_126: 1.26.10 - WEAVIATE_127: 1.27.3 + WEAVIATE_125: 1.25.28 + WEAVIATE_126: 1.26.13 + WEAVIATE_127: 1.27.8 + WEAVIATE_128: 1.28.1-77a2178 jobs: checks: @@ -37,9 +38,10 @@ jobs: { node: "22.x", weaviate: $WEAVIATE_124}, { node: "22.x", weaviate: $WEAVIATE_125}, { node: "22.x", weaviate: $WEAVIATE_126}, - { node: "18.x", weaviate: $WEAVIATE_127}, - { node: "20.x", weaviate: $WEAVIATE_127}, - { node: "22.x", weaviate: $WEAVIATE_127} + { node: "22.x", weaviate: $WEAVIATE_127}, + { node: "18.x", weaviate: $WEAVIATE_128}, + { node: "20.x", weaviate: $WEAVIATE_128}, + { node: "22.x", weaviate: $WEAVIATE_128} ] steps: - uses: actions/checkout@v3 diff --git a/ci/docker-compose-proxy.yml b/ci/docker-compose-proxy.yml index 569eab3b..cefa0a28 100644 --- a/ci/docker-compose-proxy.yml +++ b/ci/docker-compose-proxy.yml @@ -26,16 +26,6 @@ services: AUTOSCHEMA_ENABLED: 'false' DISABLE_TELEMETRY: 'true' GRPC_PORT: 8021 - contextionary: - environment: - OCCURRENCE_WEIGHT_LINEAR_FACTOR: 0.75 - EXTENSIONS_STORAGE_MODE: weaviate - EXTENSIONS_STORAGE_ORIGIN: http://weaviate-proxy:8020 - NEIGHBOR_OCCURRENCE_IGNORE_PERCENTILE: 5 - ENABLE_COMPOUND_SPLITTING: 'false' - image: semitechnologies/contextionary:en0.16.0-v1.2.0 - ports: - - 9999:9999 proxy-http: image: envoyproxy/envoy:v1.29-latest command: envoy --config-path /etc/envoy/http.yaml diff --git a/src/collections/config/types/index.ts b/src/collections/config/types/index.ts index 289d93b4..8e5e1fb2 100644 --- a/src/collections/config/types/index.ts +++ b/src/collections/config/types/index.ts @@ -41,7 +41,10 @@ export type MultiTenancyConfig = { enabled: boolean; }; -export type ReplicationDeletionStrategy = 'DeleteOnConflict' | 'NoAutomatedResolution'; +export type ReplicationDeletionStrategy = + | 'DeleteOnConflict' + | 'NoAutomatedResolution' + | 'TimeBasedResolution'; export type ReplicationConfig = { asyncEnabled: boolean; diff --git a/src/collections/integration.test.ts b/src/collections/integration.test.ts index ae62d49f..bd6512fe 100644 --- a/src/collections/integration.test.ts +++ b/src/collections/integration.test.ts @@ -582,9 +582,7 @@ describe('Testing of the collections.create method', () => { expect(response.replication.asyncEnabled).toEqual(false); expect(response.replication.deletionStrategy).toEqual( - (await cluster.getWeaviateVersion().then((ver) => ver.isLowerThan(1, 25, 0))) - ? 'NoAutomatedResolution' - : 'DeleteOnConflict' + 'NoAutomatedResolution' ); expect(response.replication.factor).toEqual(2); diff --git a/src/connection/helpers.ts b/src/connection/helpers.ts index b556bb3a..f89ae5dc 100644 --- a/src/connection/helpers.ts +++ b/src/connection/helpers.ts @@ -157,13 +157,15 @@ export function connectToCustom( } function addWeaviateEmbeddingServiceHeaders(clusterURL: string, options?: ConnectToWeaviateCloudOptions) { - if (!isApiKey(options?.authCredentials)) { + const creds = options?.authCredentials; + + if (!isApiKey(creds)) { return options?.headers; } return { - ...options.headers, - 'X-Weaviate-Api-Key': mapApiKey(options.authCredentials).apiKey, + ...options?.headers, + 'X-Weaviate-Api-Key': mapApiKey(creds).apiKey, 'X-Weaviate-Cluster-Url': clusterURL, }; } diff --git a/src/connection/http.ts b/src/connection/http.ts index aa8b91f3..c18efb58 100644 --- a/src/connection/http.ts +++ b/src/connection/http.ts @@ -4,8 +4,10 @@ import { Agent } from 'http'; import OpenidConfigurationGetter from '../misc/openidConfigurationGetter.js'; import { + WeaviateInsufficientPermissionsError, WeaviateInvalidInputError, WeaviateRequestTimeoutError, + WeaviateUnauthenticatedError, WeaviateUnexpectedStatusCodeError, } from '../errors.js'; import { @@ -155,11 +157,11 @@ export default class ConnectionREST { return this.http.head(path, payload); }; - get = (path: string, expectReturnContent = true) => { + get = (path: string, expectReturnContent = true) => { if (this.authEnabled) { - return this.login().then((token) => this.http.get(path, expectReturnContent, token)); + return this.login().then((token) => this.http.get(path, expectReturnContent, token)); } - return this.http.get(path, expectReturnContent); + return this.http.get(path, expectReturnContent); }; login = async () => { @@ -197,7 +199,7 @@ export interface HttpClient { expectReturnContent: boolean, bearerToken: string ) => Promise; - get: (path: string, expectReturnContent?: boolean, bearerToken?: string) => any; + get: (path: string, expectReturnContent?: boolean, bearerToken?: string) => Promise; externalPost: (externalUrl: string, body: any, contentType: any) => any; getRaw: (path: string, bearerToken?: string) => any; delete: (path: string, payload: any, expectReturnContent?: boolean, bearerToken?: string) => any; @@ -313,7 +315,7 @@ export const httpClient = (config: InternalConnectionParams): HttpClient => { handleHeadResponse(false) ); }, - get: (path: string, expectReturnContent = true, bearerToken = ''): Promise => { + get: (path: string, expectReturnContent = true, bearerToken = ''): Promise => { const request = { method: 'GET', headers: { @@ -323,7 +325,7 @@ export const httpClient = (config: InternalConnectionParams): HttpClient => { }; addAuthHeaderIfNeeded(request, bearerToken); return fetchWithTimeout(url(path), config.timeout?.query || 30, request).then( - checkStatus(expectReturnContent) + checkStatus(expectReturnContent) ); }, getRaw: (path: string, bearerToken = '') => { @@ -380,7 +382,13 @@ const checkStatus = } catch (e) { err = errText; } - return Promise.reject(new WeaviateUnexpectedStatusCodeError(res.status, err)); + if (res.status === 401) { + return Promise.reject(new WeaviateUnauthenticatedError(err)); + } else if (res.status === 403) { + return Promise.reject(new WeaviateInsufficientPermissionsError(403, err)); + } else { + return Promise.reject(new WeaviateUnexpectedStatusCodeError(res.status, err)); + } }); } if (expectResponseBody) { diff --git a/src/errors.ts b/src/errors.ts index ccff65a7..ad1a21e9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -148,3 +148,20 @@ export class WeaviateRequestTimeoutError extends WeaviateError { super(`Weaviate request timed out with message: ${message}`); } } + +/** + * Is thrown if a request to Weaviate fails with a forbidden status code due to insufficient permissions. + */ +export class WeaviateInsufficientPermissionsError extends WeaviateError { + public code: number; + constructor(code: number, message: string) { + super(`Forbidden: ${message}`); + this.code = code; + } +} + +export class WeaviateUnauthenticatedError extends WeaviateError { + constructor(message: string) { + super(`Unauthenticated: ${message}`); + } +} diff --git a/src/grpc/batcher.ts b/src/grpc/batcher.ts index eedf3d8b..51179042 100644 --- a/src/grpc/batcher.ts +++ b/src/grpc/batcher.ts @@ -1,4 +1,4 @@ -import { Metadata } from 'nice-grpc'; +import { Metadata, ServerError, Status } from 'nice-grpc'; import { ConsistencyLevel } from '../data/index.js'; @@ -6,7 +6,11 @@ import { BatchObject, BatchObjectsReply, BatchObjectsRequest } from '../proto/v1 import { WeaviateClient } from '../proto/v1/weaviate.js'; import { RetryOptions } from 'nice-grpc-client-middleware-retry'; -import { WeaviateBatchError, WeaviateDeleteManyError } from '../errors.js'; +import { + WeaviateBatchError, + WeaviateDeleteManyError, + WeaviateInsufficientPermissionsError, +} from '../errors.js'; import { Filters } from '../proto/v1/base.js'; import { BatchDeleteReply, BatchDeleteRequest } from '../proto/v1/batch_delete.js'; import Base from './base.js'; @@ -58,6 +62,9 @@ export default class Batcher extends Base implements Batch { } ) ).catch((err) => { + if (err instanceof ServerError && err.code === Status.PERMISSION_DENIED) { + throw new WeaviateInsufficientPermissionsError(7, err.message); + } throw new WeaviateDeleteManyError(err.message); }); } @@ -77,6 +84,9 @@ export default class Batcher extends Base implements Batch { } ) .catch((err) => { + if (err instanceof ServerError && err.code === Status.PERMISSION_DENIED) { + throw new WeaviateInsufficientPermissionsError(7, err.message); + } throw new WeaviateBatchError(err.message); }) ); diff --git a/src/grpc/searcher.ts b/src/grpc/searcher.ts index d69d9965..f3c1cf01 100644 --- a/src/grpc/searcher.ts +++ b/src/grpc/searcher.ts @@ -1,6 +1,6 @@ import { ConsistencyLevel } from '../data/index.js'; -import { Metadata } from 'nice-grpc'; +import { Metadata, ServerError, Status } from 'nice-grpc'; import { Filters } from '../proto/v1/base.js'; import { BM25, @@ -9,8 +9,8 @@ import { MetadataRequest, NearAudioSearch, NearDepthSearch, - NearImageSearch, NearIMUSearch, + NearImageSearch, NearObject, NearTextSearch, NearThermalSearch, @@ -25,7 +25,7 @@ import { import { WeaviateClient } from '../proto/v1/weaviate.js'; import { RetryOptions } from 'nice-grpc-client-middleware-retry'; -import { WeaviateQueryError } from '../errors.js'; +import { WeaviateInsufficientPermissionsError, WeaviateQueryError } from '../errors.js'; import { GenerativeSearch } from '../proto/v1/generative.js'; import Base from './base.js'; import { retryOptions } from './retry.js'; @@ -157,6 +157,9 @@ export default class Searcher extends Base implements Search { } ) .catch((err) => { + if (err instanceof ServerError && err.code === Status.PERMISSION_DENIED) { + throw new WeaviateInsufficientPermissionsError(7, err.message); + } throw new WeaviateQueryError(err.message, 'gRPC'); }) ); diff --git a/src/grpc/tenantsManager.ts b/src/grpc/tenantsManager.ts index 6c021535..2e21d6f4 100644 --- a/src/grpc/tenantsManager.ts +++ b/src/grpc/tenantsManager.ts @@ -1,5 +1,6 @@ -import { Metadata } from 'nice-grpc'; +import { Metadata, ServerError, Status } from 'nice-grpc'; import { RetryOptions } from 'nice-grpc-client-middleware-retry'; +import { WeaviateDeleteManyError, WeaviateInsufficientPermissionsError } from '../errors.js'; import { TenantsGetReply, TenantsGetRequest } from '../proto/v1/tenants.js'; import { WeaviateClient } from '../proto/v1/weaviate.js'; import Base from './base.js'; @@ -28,17 +29,24 @@ export default class TenantsManager extends Base implements Tenants { private call(message: TenantsGetRequest) { return this.sendWithTimeout((signal: AbortSignal) => - this.connection.tenantsGet( - { - ...message, - collection: this.collection, - }, - { - metadata: this.metadata, - signal, - ...retryOptions, - } - ) + this.connection + .tenantsGet( + { + ...message, + collection: this.collection, + }, + { + metadata: this.metadata, + signal, + ...retryOptions, + } + ) + .catch((err) => { + if (err instanceof ServerError && err.code === Status.PERMISSION_DENIED) { + throw new WeaviateInsufficientPermissionsError(7, err.message); + } + throw new WeaviateDeleteManyError(err.message); + }) ); } } diff --git a/src/index.ts b/src/index.ts index 99714153..101bc14e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import { ProxiesParams, TimeoutParams } from './connection/http.js'; import { ConnectionGRPC } from './connection/index.js'; import MetaGetter from './misc/metaGetter.js'; import { Meta } from './openapi/types.js'; +import roles, { Roles, permissions } from './roles/index.js'; import { DbVersion } from './utils/dbVersion.js'; import { Agent as HttpAgent } from 'http'; @@ -103,6 +104,7 @@ export interface WeaviateClient { cluster: Cluster; collections: Collections; oidcAuth?: OidcAuthenticator; + roles: Roles; close: () => Promise; getMeta: () => Promise; @@ -221,6 +223,7 @@ async function client(params: ClientParams): Promise { backup: backup(connection), cluster: cluster(connection), collections: collections(connection, dbVersionSupport), + roles: roles(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(), @@ -247,11 +250,13 @@ const app = { configure, configGuards, reconfigure, + permissions, }; export default app; export * from './collections/index.js'; export * from './connection/index.js'; +export * from './roles/types.js'; export * from './utils/base64.js'; export * from './utils/uuid.js'; export { diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index 5fa0f257..986f4f85 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -5,7 +5,7 @@ export interface paths { '/': { - /** Home. Discover the REST API */ + /** Get links to other endpoints to help discover the REST API */ get: operations['weaviate.root']; }; '/.well-known/live': { @@ -40,68 +40,100 @@ export interface paths { }; }; }; + '/authz/roles': { + get: operations['getRoles']; + post: operations['createRole']; + }; + '/authz/roles/{id}/add-permissions': { + post: operations['addPermissions']; + }; + '/authz/roles/{id}/remove-permissions': { + post: operations['removePermissions']; + }; + '/authz/roles/{id}': { + get: operations['getRole']; + delete: operations['deleteRole']; + }; + '/authz/roles/{id}/has-permission': { + post: operations['hasPermission']; + }; + '/authz/roles/{id}/users': { + get: operations['getUsersForRole']; + }; + '/authz/users/{id}/roles': { + get: operations['getRolesForUser']; + }; + '/authz/users/{id}/assign': { + post: operations['assignRole']; + }; + '/authz/users/{id}/revoke': { + post: operations['revokeRole']; + }; + '/authz/users/own-roles': { + get: operations['getRolesForOwnUser']; + }; '/objects': { /** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */ get: operations['objects.list']; - /** Registers a new Object. Provided meta-data and schema values are validated. */ + /** Create a new object.

Meta-data and schema values are validated.

**Note: Use `/batch` for importing many objects**:
If you plan on importing a large number of objects, it's much more efficient to use the `/batch` endpoint. Otherwise, sending multiple single requests sequentially would incur a large performance penalty.

**Note: idempotence of `/objects`**:
POST /objects will fail if an id is provided which already exists in the class. To update an existing object with the objects endpoint, use the PUT or PATCH method. */ post: operations['objects.create']; }; '/objects/{id}': { - /** Lists Objects. */ + /** Get a specific object based on its UUID. Also available as Websocket bus. */ get: operations['objects.get']; - /** Updates an Object's data. Given meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ + /** Updates an object based on its UUID. Given meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ put: operations['objects.update']; - /** Deletes an Object from the system. */ + /** Deletes an object from the database based on its UUID. */ delete: operations['objects.delete']; - /** Checks if an Object exists in the system. */ + /** Checks if an object exists in the system based on its UUID. */ head: operations['objects.head']; - /** Updates an Object. This method supports json-merge style patch semantics (RFC 7396). Provided meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ + /** Update an object based on its UUID (using patch semantics). This method supports json-merge style patch semantics (RFC 7396). Provided meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ patch: operations['objects.patch']; }; '/objects/{className}/{id}': { - /** Get a single data object */ + /** Get a data object based on its collection and UUID. Also available as Websocket bus. */ get: operations['objects.class.get']; - /** Update an individual data object based on its class and uuid. */ + /** Update an object based on its uuid and collection. This (`put`) method replaces the object with the provided object. */ put: operations['objects.class.put']; - /** Delete a single data object. */ + /** Delete an object based on its collection and UUID.

Note: For backward compatibility, beacons also support an older, deprecated format without the collection name. As a result, when deleting a reference, the beacon specified has to match the beacon to be deleted exactly. In other words, if a beacon is present using the old format (without collection name) you also need to specify it the same way.

In the beacon format, you need to always use `localhost` as the host, rather than the actual hostname. `localhost` here refers to the fact that the beacon's target is on the same Weaviate instance, as opposed to a foreign instance. */ delete: operations['objects.class.delete']; - /** Checks if a data object exists without retrieving it. */ + /** Checks if a data object exists based on its collection and uuid without retrieving it.

Internally it skips reading the object from disk other than checking if it is present. Thus it does not use resources on marshalling, parsing, etc., and is faster. Note the resulting HTTP request has no body; the existence of an object is indicated solely by the status code. */ head: operations['objects.class.head']; /** Update an individual data object based on its class and uuid. This method supports json-merge style patch semantics (RFC 7396). Provided meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ patch: operations['objects.class.patch']; }; '/objects/{id}/references/{propertyName}': { - /** Replace all references to a class-property. */ + /** Replace all references in cross-reference property of an object. */ put: operations['objects.references.update']; - /** Add a single reference to a class-property. */ + /** Add a cross-reference. */ post: operations['objects.references.create']; /** Delete the single reference that is given in the body from the list of references that this property has. */ delete: operations['objects.references.delete']; }; '/objects/{className}/{id}/references/{propertyName}': { - /** Update all references of a property of a data object. */ + /** Replace **all** references in cross-reference property of an object. */ put: operations['objects.class.references.put']; - /** Add a single reference to a class-property. */ + /** Add a single reference to an object. This adds a reference to the array of cross-references of the given property in the source object specified by its collection name and id */ post: operations['objects.class.references.create']; - /** Delete the single reference that is given in the body from the list of references that this property of a data object has */ + /** Delete the single reference that is given in the body from the list of references that this property has. */ delete: operations['objects.class.references.delete']; }; '/objects/validate': { - /** Validate an Object's schema and meta-data. It has to be based on a schema, which is related to the given Object to be accepted by this validation. */ + /** Validate an object's schema and meta-data without creating it.

If the schema of the object is valid, the request should return nothing with a plain RESTful request. Otherwise, an error object will be returned. */ post: operations['objects.validate']; }; '/batch/objects': { - /** Register new Objects in bulk. Provided meta-data and schema values are validated. */ + /** Create new objects in bulk.

Meta-data and schema values are validated.

**Note: idempotence of `/batch/objects`**:
`POST /batch/objects` is idempotent, and will overwrite any existing object given the same id. */ post: operations['batch.objects.create']; - /** Delete Objects in bulk that match a certain filter. */ + /** Batch delete objects that match a particular filter.

The request body takes a single `where` filter and will delete all objects matched.

Note that there is a limit to the number of objects to be deleted at once using this filter, in order to protect against unexpected memory surges and very-long-running requests. The default limit is 10,000 and may be configured by setting the `QUERY_MAXIMUM_RESULTS` environment variable.

Objects are deleted in the same order that they would be returned in an equivalent Get query. To delete more objects than the limit, run the same query multiple times. */ delete: operations['batch.objects.delete']; }; '/batch/references': { - /** Register cross-references between any class items (objects or objects) in bulk. */ + /** Batch create cross-references between collections items (objects or objects) in bulk. */ post: operations['batch.references.create']; }; '/graphql': { - /** Get an object based on GraphQL */ + /** Get a response based on a GraphQL query */ post: operations['graphql.post']; }; '/graphql/batch': { @@ -109,27 +141,31 @@ export interface paths { post: operations['graphql.batch']; }; '/meta': { - /** Gives meta information about the server and can be used to provide information to another Weaviate instance that wants to interact with the current instance. */ + /** Returns meta information about the server. Can be used to provide information to another Weaviate instance that wants to interact with the current instance. */ get: operations['meta.get']; }; '/schema': { + /** Fetch an array of all collection definitions from the schema. */ get: operations['schema.dump']; + /** Create a new data object collection.

If AutoSchema is enabled, Weaviate will attempt to infer the schema from the data at import time. However, manual schema definition is recommended for production environments. */ post: operations['schema.objects.create']; }; '/schema/{className}': { get: operations['schema.objects.get']; - /** Use this endpoint to alter an existing class in the schema. Note that not all settings are mutable. If an error about immutable fields is returned and you still need to update this particular setting, you will have to delete the class (and the underlying data) and recreate. This endpoint cannot be used to modify properties. Instead use POST /v1/schema/{className}/properties. A typical use case for this endpoint is to update configuration, such as the vectorIndexConfig. Note that even in mutable sections, such as vectorIndexConfig, some fields may be immutable. */ + /** Add a property to an existing collection. */ put: operations['schema.objects.update']; + /** Remove a collection from the schema. This will also delete all the objects in the collection. */ delete: operations['schema.objects.delete']; }; '/schema/{className}/properties': { post: operations['schema.objects.properties.add']; }; '/schema/{className}/shards': { + /** Get the status of every shard in the cluster. */ get: operations['schema.objects.shards.get']; }; '/schema/{className}/shards/{shardName}': { - /** Update shard status of an Object Class */ + /** Update a shard status for a collection. For example, a shard may have been marked as `READONLY` because its disk was full. After providing more disk space, use this endpoint to set the shard status to `READY` again. There is also a convenience function in each client to set the status of all shards of a collection. */ put: operations['schema.objects.shards.update']; }; '/schema/{className}/tenants': { @@ -137,31 +173,33 @@ export interface paths { get: operations['tenants.get']; /** Update tenant of a specific class */ put: operations['tenants.update']; - /** Create a new tenant for a specific class */ + /** Create a new tenant for a collection. Multi-tenancy must be enabled in the collection definition. */ post: operations['tenants.create']; /** delete tenants from a specific class */ delete: operations['tenants.delete']; }; '/schema/{className}/tenants/{tenantName}': { + /** get a specific tenant for the given class */ + get: operations['tenants.get.one']; /** Check if a tenant exists for a specific class */ head: operations['tenant.exists']; }; '/backups/{backend}': { /** [Coming soon] List all backups in progress not implemented yet. */ get: operations['backups.list']; - /** Starts a process of creating a backup for a set of classes */ + /** Start creating a backup for a set of collections.

Notes:
- Weaviate uses gzip compression by default.
- Weaviate stays usable while a backup process is ongoing. */ post: operations['backups.create']; }; '/backups/{backend}/{id}': { - /** Returns status of backup creation attempt for a set of classes */ + /** Returns status of backup creation attempt for a set of collections.

All client implementations have a `wait for completion` option which will poll the backup status in the background and only return once the backup has completed (successfully or unsuccessfully). If you set the `wait for completion` option to false, you can also check the status yourself using this endpoint. */ get: operations['backups.create.status']; /** Cancel created backup with specified ID */ delete: operations['backups.cancel']; }; '/backups/{backend}/{id}/restore': { - /** Returns status of a backup restoration attempt for a set of classes */ + /** Returns status of a backup restoration attempt for a set of classes.

All client implementations have a `wait for completion` option which will poll the backup status in the background and only return once the backup has completed (successfully or unsuccessfully). If you set the `wait for completion` option to false, you can also check the status yourself using the this endpoint. */ get: operations['backups.restore.status']; - /** Starts a process of restoring a backup for a set of classes */ + /** Starts a process of restoring a backup for a set of collections.

Any backup can be restored to any machine, as long as the number of nodes between source and target are identical.

Requrements:

- None of the collections to be restored already exist on the target restoration node(s).
- The node names of the backed-up collections' must match those of the target restoration node(s). */ post: operations['backups.restore']; }; '/cluster/statistics': { @@ -169,11 +207,11 @@ export interface paths { get: operations['cluster.get.statistics']; }; '/nodes': { - /** Returns status of Weaviate DB. */ + /** Returns node information for the entire database. */ get: operations['nodes.get']; }; '/nodes/{className}': { - /** Returns status of Weaviate DB. */ + /** Returns node information for the nodes relevant to the collection. */ get: operations['nodes.get.class']; }; '/classifications/': { @@ -187,6 +225,97 @@ export interface paths { } export interface definitions { + Role: { + /** @description role name */ + name: string; + permissions: definitions['Permission'][]; + }; + /** @description permissions attached to a role. */ + Permission: { + /** @description resources applicable for backup actions */ + backups?: { + /** + * @description string or regex. if a specific collection name, if left empty it will be ALL or * + * @default * + */ + collection?: string; + }; + /** @description resources applicable for data actions */ + data?: { + /** + * @description string or regex. if a specific collection name, if left empty it will be ALL or * + * @default * + */ + collection?: string; + /** + * @description string or regex. if a specific tenant name, if left empty it will be ALL or * + * @default * + */ + tenant?: string; + /** + * @description string or regex. if a specific object ID, if left empty it will be ALL or * + * @default * + */ + object?: string; + }; + /** @description resources applicable for cluster actions */ + nodes?: { + /** + * @description whether to allow (verbose) returning shards and stats data in the response + * @default minimal + * @enum {string} + */ + verbosity?: 'verbose' | 'minimal'; + /** + * @description string or regex. if a specific collection name, if left empty it will be ALL or * + * @default * + */ + collection?: string; + }; + /** @description resources applicable for role actions */ + roles?: { + /** + * @description string or regex. if a specific role name, if left empty it will be ALL or * + * @default * + */ + role?: string; + }; + /** @description resources applicable for collection and/or tenant actions */ + collections?: { + /** + * @description string or regex. if a specific collection name, if left empty it will be ALL or * + * @default * + */ + collection?: string; + /** + * @description string or regex. if a specific tenant name, if left empty it will be ALL or * + * @default * + */ + tenant?: string; + }; + /** + * @description allowed actions in weaviate. + * @enum {string} + */ + action: + | 'manage_backups' + | 'read_cluster' + | 'manage_data' + | 'create_data' + | 'read_data' + | 'update_data' + | 'delete_data' + | 'read_nodes' + | 'manage_roles' + | 'read_roles' + | 'manage_collections' + | 'create_collections' + | 'read_collections' + | 'update_collections' + | 'delete_collections'; + }; + /** @description list of roles */ + RolesListResponse: definitions['Role'][]; Link: { /** @description target of the link */ href?: string; @@ -242,11 +371,11 @@ export interface definitions { /** Format: float */ distance?: number; }[]; - /** @description A Vector in the Contextionary */ + /** @description A vector representation of the object in the Contextionary. If provided at object creation, this wil take precedence over any vectorizer setting. */ C11yVector: number[]; - /** @description A Vector object */ + /** @description A vector representation of the object. If provided at object creation, this wil take precedence over any vectorizer setting. */ Vector: number[]; - /** @description A Multi Vector map of named vectors */ + /** @description A map of named vectors for multi-vector representations. */ Vectors: { [key: string]: definitions['Vector'] }; /** @description Receive question based on array of classes, properties and values. */ C11yVectorBasedQuestion: { @@ -326,7 +455,7 @@ export interface definitions { }; /** @description A list of GraphQL responses. */ GraphQLResponses: definitions['GraphQLResponse'][]; - /** @description Configure the inverted index built into Weaviate */ + /** @description Configure the inverted index built into Weaviate (default: 60). */ InvertedIndexConfig: { /** * Format: int @@ -335,54 +464,54 @@ export interface definitions { cleanupIntervalSeconds?: number; bm25?: definitions['BM25Config']; stopwords?: definitions['StopwordConfig']; - /** @description Index each object by its internal timestamps */ + /** @description Index each object by its internal timestamps (default: 'false'). */ indexTimestamps?: boolean; - /** @description Index each object with the null state */ + /** @description Index each object with the null state (default: 'false'). */ indexNullState?: boolean; - /** @description Index length of properties */ + /** @description Index length of properties (default: 'false'). */ indexPropertyLength?: boolean; }; /** @description Configure how replication is executed in a cluster */ ReplicationConfig: { - /** @description Number of times a class is replicated */ + /** @description Number of times a class is replicated (default: 1). */ factor?: number; - /** @description Enable asynchronous replication */ + /** @description Enable asynchronous replication (default: false). */ asyncEnabled?: boolean; /** - * @description Conflict resolution strategy for deleted objects + * @description Conflict resolution strategy for deleted objects. * @enum {string} */ - deletionStrategy?: 'NoAutomatedResolution' | 'DeleteOnConflict'; + deletionStrategy?: 'NoAutomatedResolution' | 'DeleteOnConflict' | 'TimeBasedResolution'; }; /** @description tuning parameters for the BM25 algorithm */ BM25Config: { /** * Format: float - * @description calibrates term-weight scaling based on the term frequency within a document + * @description Calibrates term-weight scaling based on the term frequency within a document (default: 1.2). */ k1?: number; /** * Format: float - * @description calibrates term-weight scaling based on the document length + * @description Calibrates term-weight scaling based on the document length (default: 0.75). */ b?: number; }; /** @description fine-grained control over stopword list usage */ StopwordConfig: { - /** @description pre-existing list of common words by language */ + /** @description Pre-existing list of common words by language (default: 'en'). Options: ['en', 'none']. */ preset?: string; - /** @description stopwords to be considered additionally */ + /** @description Stopwords to be considered additionally (default: []). Can be any array of custom strings. */ additions?: string[]; - /** @description stopwords to be removed from consideration */ + /** @description Stopwords to be removed from consideration (default: []). Can be any array of custom strings. */ removals?: string[]; }; /** @description Configuration related to multi-tenancy within a class */ MultiTenancyConfig: { - /** @description Whether or not multi-tenancy is enabled for this class */ + /** @description Whether or not multi-tenancy is enabled for this class (default: false). */ enabled?: boolean; - /** @description Nonexistent tenants should (not) be created implicitly */ + /** @description Nonexistent tenants should (not) be created implicitly (default: false). */ autoTenantCreation?: boolean; - /** @description Existing tenants should (not) be turned HOT implicitly when they are accessed and in another activity status */ + /** @description Existing tenants should (not) be turned HOT implicitly when they are accessed and in another activity status (default: false). */ autoTenantActivation?: boolean; }; /** @description JSON object value. */ @@ -394,11 +523,11 @@ export interface definitions { * @description The url of the host. */ hostname?: string; - /** @description Version of weaviate you are currently running */ + /** @description The Weaviate server version. */ version?: string; - /** @description Module-specific meta information */ + /** @description Module-specific meta information. */ modules?: { [key: string]: unknown }; - /** @description Max message size for GRPC connection in bytes */ + /** @description Max message size for GRPC connection in bytes. */ grpcMaxMessageSize?: number; }; /** @description Multiple instances of references to other objects. */ @@ -454,7 +583,7 @@ export interface definitions { PeerUpdateList: definitions['PeerUpdate'][]; /** @description Allow custom overrides of vector weights as math expressions. E.g. "pancake": "7" will set the weight for the word pancake to 7 in the vectorization, whereas "w * 3" would triple the originally calculated word. This is an open object, with OpenAPI Specification 3.0 this will be more detailed. See Weaviate docs for more info. In the future this will become a key/value (string/string) object. */ VectorWeights: { [key: string]: unknown }; - /** @description This is an open object, with OpenAPI Specification 3.0 this will be more detailed. See Weaviate docs for more info. In the future this will become a key/value OR a SingleRef definition. */ + /** @description Names and values of an individual property. A returned response may also contain additional metadata, such as from classification or feature projection. */ PropertySchema: { [key: string]: unknown }; /** @description This is an open object, with OpenAPI Specification 3.0 this will be more detailed. See Weaviate docs for more info. In the future this will become a key/value OR a SingleRef definition. */ SchemaHistory: { [key: string]: unknown }; @@ -487,8 +616,9 @@ export interface definitions { ignoreSchemaSync?: boolean; }; Class: { - /** @description Name of the class as URI relative to the schema URL. */ + /** @description Name of the class (a.k.a. 'collection') (required). Multiple words should be concatenated in CamelCase, e.g. `ArticleAuthor`. */ class?: string; + /** @description Configure named vectors. Either use this field or `vectorizer`, `vectorIndexType`, and `vectorIndexConfig` fields. Available from `v1.24.0`. */ vectorConfig?: { [key: string]: definitions['VectorConfig'] }; /** @description Name of the vector index to use, eg. (HNSW) */ vectorIndexType?: string; @@ -501,35 +631,43 @@ export interface definitions { multiTenancyConfig?: definitions['MultiTenancyConfig']; /** @description Specify how the vectors for this class should be determined. The options are either 'none' - this means you have to import a vector with each object yourself - or the name of a module that provides vectorization capabilities, such as 'text2vec-contextionary'. If left empty, it will use the globally configured default which can itself either be 'none' or a specific module. */ vectorizer?: string; - /** @description Configuration specific to modules this Weaviate instance has installed */ + /** @description Configuration specific to modules in a collection context. */ moduleConfig?: { [key: string]: unknown }; - /** @description Description of the class. */ + /** @description Description of the collection for metadata purposes. */ description?: string; - /** @description The properties of the class. */ + /** @description Define properties of the collection. */ properties?: definitions['Property'][]; }; Property: { - /** @description Can be a reference to another type when it starts with a capital (for example Person), otherwise "string" or "int". */ + /** @description Data type of the property (required). If it starts with a capital (for example Person), may be a reference to another type. */ dataType?: string[]; /** @description Description of the property. */ description?: string; /** @description Configuration specific to modules this Weaviate instance has installed */ moduleConfig?: { [key: string]: unknown }; - /** @description Name of the property as URI relative to the schema URL. */ + /** @description The name of the property (required). Multiple words should be concatenated in camelCase, e.g. `nameOfAuthor`. */ name?: string; - /** @description Optional. Should this property be indexed in the inverted index. Defaults to true. If you choose false, you will not be able to use this property in where filters, bm25 or hybrid search. This property has no affect on vectorization decisions done by modules (deprecated as of v1.19; use indexFilterable or/and indexSearchable instead) */ + /** @description (Deprecated). Whether to include this property in the inverted index. If `false`, this property cannot be used in `where` filters, `bm25` or `hybrid` search.

Unrelated to vectorization behavior (deprecated as of v1.19; use indexFilterable or/and indexSearchable instead) */ indexInverted?: boolean; - /** @description Optional. Should this property be indexed in the inverted index. Defaults to true. If you choose false, you will not be able to use this property in where filters. This property has no affect on vectorization decisions done by modules */ + /** @description Whether to include this property in the filterable, Roaring Bitmap index. If `false`, this property cannot be used in `where` filters.

Note: Unrelated to vectorization behavior. */ indexFilterable?: boolean; /** @description Optional. Should this property be indexed in the inverted index. Defaults to true. Applicable only to properties of data type text and text[]. If you choose false, you will not be able to use this property in bm25 or hybrid search. This property has no affect on vectorization decisions done by modules */ indexSearchable?: boolean; - /** @description Optional. Should this property be indexed in the inverted index. Defaults to false. Provides better performance for range queries compared to filterable index in large datasets. Applicable only to properties of data type int, number, date. */ + /** @description Whether to include this property in the filterable, range-based Roaring Bitmap index. Provides better performance for range queries compared to filterable index in large datasets. Applicable only to properties of data type int, number, date. */ indexRangeFilters?: boolean; /** * @description Determines tokenization of the property as separate words or whole field. Optional. Applies to text and text[] data types. Allowed values are `word` (default; splits on any non-alphanumerical, lowercases), `lowercase` (splits on white spaces, lowercases), `whitespace` (splits on white spaces), `field` (trims). Not supported for remaining data types * @enum {string} */ - tokenization?: 'word' | 'lowercase' | 'whitespace' | 'field' | 'trigram' | 'gse' | 'kagome_kr'; + tokenization?: + | 'word' + | 'lowercase' + | 'whitespace' + | 'field' + | 'trigram' + | 'gse' + | 'kagome_kr' + | 'kagome_ja'; /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; @@ -549,7 +687,16 @@ export interface definitions { indexSearchable?: boolean; indexRangeFilters?: boolean; /** @enum {string} */ - tokenization?: 'word' | 'lowercase' | 'whitespace' | 'field'; + tokenization?: + | 'word' + | 'lowercase' + | 'whitespace' + | 'field' + | 'trigram' + | 'gse' + | 'kagome_kr' + | 'kagome_ja'; + /** @description The properties of the nested object(s). Applies to object and object[] data types. */ nestedProperties?: definitions['NestedProperty'][]; }; /** @description The status of all the shards of a Class */ @@ -591,7 +738,7 @@ export interface definitions { id?: string; /** @description Backup backend name e.g. filesystem, gcs, s3. */ backend?: string; - /** @description destination path of backup files proper to selected backup backend */ + /** @description destination path of backup files proper to selected backup backend, contains bucket and path */ path?: string; /** @description error message if restoration failed */ error?: string; @@ -604,13 +751,19 @@ export interface definitions { }; /** @description Backup custom configuration */ BackupConfig: { + /** @description name of the endpoint, e.g. s3.amazonaws.com */ + Endpoint?: string; + /** @description Name of the bucket, container, volume, etc */ + Bucket?: string; + /** @description Path or key within the bucket */ + Path?: string; /** * @description Desired CPU core utilization ranging from 1%-80% * @default 50 */ CPUPercentage?: number; /** - * @description Weaviate will attempt to come close the specified size, with a minimum of 2MB, default of 128MB, and a maximum of 512MB + * @description Aimed chunk size, with a minimum of 2MB, default of 128MB, and a maximum of 512MB. The actual chunk size may vary. * @default 128 */ ChunkSize?: number; @@ -623,6 +776,12 @@ export interface definitions { }; /** @description Backup custom configuration */ RestoreConfig: { + /** @description name of the endpoint, e.g. s3.amazonaws.com */ + Endpoint?: string; + /** @description Name of the bucket, container, volume, etc */ + Bucket?: string; + /** @description Path within the bucket */ + Path?: string; /** * @description Desired CPU core utilization ranging from 1%-80% * @default 50 @@ -631,13 +790,13 @@ export interface definitions { }; /** @description Request body for creating a backup of a set of classes */ BackupCreateRequest: { - /** @description The ID of the backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ + /** @description The ID of the backup (required). Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id?: string; /** @description Custom configuration for the backup creation process */ config?: definitions['BackupConfig']; - /** @description List of classes to include in the backup creation process */ + /** @description List of collections to include in the backup creation process. If not set, all collections are included. Cannot be used together with `exclude`. */ include?: string[]; - /** @description List of classes to exclude from the backup creation process */ + /** @description List of collections to exclude from the backup creation process. If not set, all collections are included. Cannot be used together with `include`. */ exclude?: string[]; }; /** @description The definition of a backup create response body */ @@ -648,7 +807,9 @@ export interface definitions { classes?: string[]; /** @description Backup backend name e.g. filesystem, gcs, s3. */ backend?: string; - /** @description destination path of backup files proper to selected backend */ + /** @description Name of the bucket, container, volume, etc */ + bucket?: string; + /** @description Path within bucket of backup */ path?: string; /** @description error message if creation failed */ error?: string; @@ -707,7 +868,7 @@ export interface definitions { NodeStats: { /** * Format: int - * @description The count of Weaviate's shards. + * @description The count of Weaviate's shards. To see this value, set `output` to `verbose`. */ shardCount?: number; /** @@ -856,7 +1017,7 @@ export interface definitions { /** @description Additional Meta information about classifications if the item was part of one */ classification?: definitions['ReferenceMetaClassification']; }; - /** @description Additional Meta information about a single object object. */ + /** @description (Response only) Additional meta information about a single object. */ AdditionalProperties: { [key: string]: { [key: string]: unknown } }; /** @description This meta field contains additional info about the classified reference property */ ReferenceMetaClassification: { @@ -937,7 +1098,7 @@ export interface definitions { * @default SUCCESS * @enum {string} */ - status?: 'SUCCESS' | 'PENDING' | 'FAILED'; + status?: 'SUCCESS' | 'FAILED'; errors?: definitions['ErrorResponse']; }; }; @@ -987,12 +1148,12 @@ export interface definitions { id?: string; /** * Format: int64 - * @description Timestamp of creation of this Object in milliseconds since epoch UTC. + * @description (Response only) Timestamp of creation of this object in milliseconds since epoch UTC. */ creationTimeUnix?: number; /** * Format: int64 - * @description Timestamp of the last Object update in milliseconds since epoch UTC. + * @description (Response only) Timestamp of the last object update in milliseconds since epoch UTC. */ lastUpdateTimeUnix?: number; /** @description This field returns vectors associated with the Object. C11yVector, Vector or Vectors values are possible. */ @@ -1015,7 +1176,7 @@ export interface definitions { * @default SUCCESS * @enum {string} */ - status?: 'SUCCESS' | 'PENDING' | 'FAILED'; + status?: 'SUCCESS' | 'FAILED'; errors?: definitions['ErrorResponse']; }; }; @@ -1036,7 +1197,12 @@ export interface definitions { */ output?: string; /** - * @description If true, objects will not be deleted yet, but merely listed. Defaults to false. + * Format: int64 + * @description Timestamp of deletion in milliseconds since epoch UTC. + */ + deletionTimeUnixMilli?: number; + /** + * @description If true, the call will show which objects would be matched using the specified filter without deleting any objects.

Depending on the configured verbosity, you will either receive a count of affected objects, or a list of IDs. * @default false */ dryRun?: boolean; @@ -1058,6 +1224,11 @@ export interface definitions { * @default minimal */ output?: string; + /** + * Format: int64 + * @description Timestamp of deletion in milliseconds since epoch UTC. + */ + deletionTimeUnixMilli?: number; /** * @description If true, objects will not be deleted yet, but merely listed. Defaults to false. * @default false @@ -1311,7 +1482,7 @@ export interface definitions { }; /** @description attributes representing a single tenant within weaviate */ Tenant: { - /** @description name of the tenant */ + /** @description The name of the tenant (required). */ name?: string; /** * @description activity status of the tenant's shard. Optional for creating tenant (implicit `ACTIVE`) and required for updating tenant. For creation, allowed values are `ACTIVE` - tenant is fully active and `INACTIVE` - tenant is inactive; no actions can be performed on tenant, tenant's files are stored locally. For updating, `ACTIVE`, `INACTIVE` and also `OFFLOADED` - as INACTIVE, but files are stored on cloud storage. The following values are read-only and are set by the server for internal use: `OFFLOADING` - tenant is transitioning from ACTIVE/INACTIVE to OFFLOADED, `ONLOADING` - tenant is transitioning from OFFLOADED to ACTIVE/INACTIVE. We still accept deprecated names `HOT` (now `ACTIVE`), `COLD` (now `INACTIVE`), `FROZEN` (now `OFFLOADED`), `FREEZING` (now `OFFLOADING`), `UNFREEZING` (now `ONLOADING`). @@ -1329,20 +1500,31 @@ export interface definitions { | 'FREEZING' | 'UNFREEZING'; }; + /** @description attributes representing a single tenant response within weaviate */ + TenantResponse: definitions['Tenant'] & { + /** @description The list of nodes that owns that tenant data. */ + belongsToNodes?: string[]; + /** + * @description Experimental. The data version of the tenant is a monotonically increasing number starting from 0 which is incremented each time a tenant's data is offloaded to cloud storage. + * @default 0 + * @example 3 + */ + dataVersion?: number; + }; } export interface parameters { - /** @description The starting ID of the result window. */ + /** @description A threshold UUID of the objects to retrieve after, using an UUID-based ordering. This object is not part of the set.

Must be used with `class`, typically in conjunction with `limit`.

Note `after` cannot be used with `offset` or `sort`.

For a null value similar to offset=0, set an empty string in the request, i.e. `after=` or `after`. */ CommonAfterParameterQuery: string; /** * Format: int64 - * @description The starting index of the result window. Default value is 0. + * @description The starting index of the result window. Note `offset` will retrieve `offset+limit` results and return `limit` results from the object with index `offset` onwards. Limited by the value of `QUERY_MAXIMUM_RESULTS`.

Should be used in conjunction with `limit`.

Cannot be used with `after`. * @default 0 */ CommonOffsetParameterQuery: number; /** * Format: int64 - * @description The maximum number of items to be returned per page. Default value is set in Weaviate config. + * @description The maximum number of items to be returned per page. The default is 25 unless set otherwise as an environment variable. */ CommonLimitParameterQuery: number; /** @description Include additional information, such as classification infos. Allowed values include: classification, vector, interpretation */ @@ -1353,11 +1535,11 @@ export interface parameters { CommonTenantParameterQuery: string; /** @description The target node which should fulfill the request */ CommonNodeNameParameterQuery: string; - /** @description Sort parameter to pass an information about the names of the sort fields */ + /** @description Name(s) of the property to sort by - e.g. `city`, or `country,city`. */ CommonSortParameterQuery: string; - /** @description Order parameter to tell how to order (asc or desc) data within given field */ + /** @description Order parameter to tell how to order (asc or desc) data within given field. Should be used in conjunction with `sort` parameter. If providing multiple `sort` values, provide multiple `order` values in corresponding order, e.g.: `sort=author_name,title&order=desc,asc`. */ CommonOrderParameterQuery: string; - /** @description Class parameter specifies the class from which to query objects */ + /** @description The collection from which to query objects.

Note that if `class` is not provided, the response will not include any objects. */ CommonClassParameterQuery: string; /** * @description Controls the verbosity of the output, possible values are: "minimal", "verbose". Defaults to "minimal". @@ -1367,7 +1549,7 @@ export interface parameters { } export interface operations { - /** Home. Discover the REST API */ + /** Get links to other endpoints to help discover the REST API */ 'weaviate.root': { responses: { /** Weaviate is alive and ready to serve content */ @@ -1394,30 +1576,396 @@ export interface operations { 503: unknown; }; }; + getRoles: { + responses: { + /** Successful response. */ + 200: { + schema: definitions['RolesListResponse']; + }; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + 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']; + }; + }; + }; + createRole: { + parameters: { + body: { + body: definitions['Role']; + }; + }; + responses: { + /** Role created successfully */ + 201: unknown; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Role already exists */ + 409: { + schema: definitions['ErrorResponse']; + }; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 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']; + }; + }; + }; + addPermissions: { + parameters: { + path: { + /** role name */ + id: string; + }; + body: { + body: { + /** @description permissions to be added to the role */ + permissions: definitions['Permission'][]; + } & { + name: unknown; + }; + }; + }; + responses: { + /** Permissions added successfully */ + 200: unknown; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role found */ + 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 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']; + }; + }; + }; + removePermissions: { + parameters: { + path: { + /** role name */ + id: string; + }; + body: { + body: { + /** @description permissions to remove from the role */ + permissions: definitions['Permission'][]; + }; + }; + }; + responses: { + /** Permissions removed successfully */ + 200: unknown; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role found */ + 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 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']; + }; + }; + }; + getRole: { + parameters: { + path: { + /** role name */ + id: string; + }; + }; + responses: { + /** Successful response. */ + 200: { + schema: definitions['Role']; + }; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role 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']; + }; + }; + }; + deleteRole: { + parameters: { + path: { + /** role name */ + id: string; + }; + }; + responses: { + /** Successfully deleted. */ + 204: never; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + 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']; + }; + }; + }; + hasPermission: { + parameters: { + path: { + /** role name */ + id: string; + }; + body: { + body: definitions['Permission']; + }; + }; + responses: { + /** Permission check was successful */ + 200: { + schema: boolean; + }; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. Are you sure the class is defined in the configuration file? */ + 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']; + }; + }; + }; + getUsersForRole: { + parameters: { + path: { + /** role name */ + id: string; + }; + }; + responses: { + /** Users assigned to this role */ + 200: { + schema: string[]; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role 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']; + }; + }; + }; + getRolesForUser: { + parameters: { + path: { + /** user name */ + id: string; + }; + }; + responses: { + /** Role assigned users */ + 200: { + schema: definitions['RolesListResponse']; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** no role found for user */ + 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']; + }; + }; + }; + assignRole: { + parameters: { + path: { + /** user name */ + id: string; + }; + body: { + body: { + /** @description the roles that assigned to user */ + roles?: string[]; + }; + }; + }; + responses: { + /** Role assigned successfully */ + 200: unknown; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** role or user is 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']; + }; + }; + }; + revokeRole: { + parameters: { + path: { + /** user name */ + id: string; + }; + body: { + body: { + /** @description the roles that revoked from the key or user */ + roles?: string[]; + }; + }; + }; + responses: { + /** Role revoked successfully */ + 200: unknown; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** role or user is 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']; + }; + }; + }; + getRolesForOwnUser: { + responses: { + /** Role assigned to own users */ + 200: { + schema: definitions['RolesListResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: 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']; + }; + }; + }; /** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */ 'objects.list': { parameters: { query: { - /** The starting ID of the result window. */ + /** A threshold UUID of the objects to retrieve after, using an UUID-based ordering. This object is not part of the set.

Must be used with `class`, typically in conjunction with `limit`.

Note `after` cannot be used with `offset` or `sort`.

For a null value similar to offset=0, set an empty string in the request, i.e. `after=` or `after`. */ after?: parameters['CommonAfterParameterQuery']; - /** The starting index of the result window. Default value is 0. */ + /** The starting index of the result window. Note `offset` will retrieve `offset+limit` results and return `limit` results from the object with index `offset` onwards. Limited by the value of `QUERY_MAXIMUM_RESULTS`.

Should be used in conjunction with `limit`.

Cannot be used with `after`. */ offset?: parameters['CommonOffsetParameterQuery']; - /** The maximum number of items to be returned per page. Default value is set in Weaviate config. */ + /** The maximum number of items to be returned per page. The default is 25 unless set otherwise as an environment variable. */ limit?: parameters['CommonLimitParameterQuery']; /** Include additional information, such as classification infos. Allowed values include: classification, vector, interpretation */ include?: parameters['CommonIncludeParameterQuery']; - /** Sort parameter to pass an information about the names of the sort fields */ + /** Name(s) of the property to sort by - e.g. `city`, or `country,city`. */ sort?: parameters['CommonSortParameterQuery']; - /** Order parameter to tell how to order (asc or desc) data within given field */ + /** Order parameter to tell how to order (asc or desc) data within given field. Should be used in conjunction with `sort` parameter. If providing multiple `sort` values, provide multiple `order` values in corresponding order, e.g.: `sort=author_name,title&order=desc,asc`. */ order?: parameters['CommonOrderParameterQuery']; - /** Class parameter specifies the class from which to query objects */ + /** The collection from which to query objects.

Note that if `class` is not provided, the response will not include any objects. */ class?: parameters['CommonClassParameterQuery']; /** Specifies the tenant in a request targeting a multi-tenant class */ tenant?: parameters['CommonTenantParameterQuery']; }; }; responses: { - /** Successful response. */ + /** Successful response.

If `class` is not provided, the response will not include any objects. */ 200: { schema: definitions['ObjectsListResponse']; }; @@ -1443,7 +1991,7 @@ export interface operations { }; }; }; - /** Registers a new Object. Provided meta-data and schema values are validated. */ + /** Create a new object.

Meta-data and schema values are validated.

**Note: Use `/batch` for importing many objects**:
If you plan on importing a large number of objects, it's much more efficient to use the `/batch` endpoint. Otherwise, sending multiple single requests sequentially would incur a large performance penalty.

**Note: idempotence of `/objects`**:
POST /objects will fail if an id is provided which already exists in the class. To update an existing object with the objects endpoint, use the PUT or PATCH method. */ 'objects.create': { parameters: { body: { @@ -1479,7 +2027,7 @@ export interface operations { }; }; }; - /** Lists Objects. */ + /** Get a specific object based on its UUID. Also available as Websocket bus. */ 'objects.get': { parameters: { path: { @@ -1514,7 +2062,7 @@ export interface operations { }; }; }; - /** Updates an Object's data. Given meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ + /** Updates an object based on its UUID. Given meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ 'objects.update': { parameters: { path: { @@ -1552,7 +2100,7 @@ export interface operations { }; }; }; - /** Deletes an Object from the system. */ + /** Deletes an object from the database based on its UUID. */ 'objects.delete': { parameters: { path: { @@ -1583,7 +2131,7 @@ export interface operations { }; }; }; - /** Checks if an Object exists in the system. */ + /** Checks if an object exists in the system based on its UUID. */ 'objects.head': { parameters: { path: { @@ -1608,7 +2156,7 @@ export interface operations { }; }; }; - /** Updates an Object. This method supports json-merge style patch semantics (RFC 7396). Provided meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ + /** Update an object based on its UUID (using patch semantics). This method supports json-merge style patch semantics (RFC 7396). Provided meta-data and schema values are validated. LastUpdateTime is set to the time this function is called. */ 'objects.patch': { parameters: { path: { @@ -1647,7 +2195,7 @@ export interface operations { }; }; }; - /** Get a single data object */ + /** Get a data object based on its collection and UUID. Also available as Websocket bus. */ 'objects.class.get': { parameters: { path: { @@ -1693,7 +2241,7 @@ export interface operations { }; }; }; - /** Update an individual data object based on its class and uuid. */ + /** Update an object based on its uuid and collection. This (`put`) method replaces the object with the provided object. */ 'objects.class.put': { parameters: { path: { @@ -1732,7 +2280,7 @@ export interface operations { }; }; }; - /** Delete a single data object. */ + /** Delete an object based on its collection and UUID.

Note: For backward compatibility, beacons also support an older, deprecated format without the collection name. As a result, when deleting a reference, the beacon specified has to match the beacon to be deleted exactly. In other words, if a beacon is present using the old format (without collection name) you also need to specify it the same way.

In the beacon format, you need to always use `localhost` as the host, rather than the actual hostname. `localhost` here refers to the fact that the beacon's target is on the same Weaviate instance, as opposed to a foreign instance. */ 'objects.class.delete': { parameters: { path: { @@ -1772,7 +2320,7 @@ export interface operations { }; }; }; - /** Checks if a data object exists without retrieving it. */ + /** Checks if a data object exists based on its collection and uuid without retrieving it.

Internally it skips reading the object from disk other than checking if it is present. Thus it does not use resources on marshalling, parsing, etc., and is faster. Note the resulting HTTP request has no body; the existence of an object is indicated solely by the status code. */ 'objects.class.head': { parameters: { path: { @@ -1852,7 +2400,7 @@ export interface operations { }; }; }; - /** Replace all references to a class-property. */ + /** Replace all references in cross-reference property of an object. */ 'objects.references.update': { parameters: { path: { @@ -1888,7 +2436,7 @@ export interface operations { }; }; }; - /** Add a single reference to a class-property. */ + /** Add a cross-reference. */ 'objects.references.create': { parameters: { path: { @@ -1960,7 +2508,7 @@ export interface operations { }; }; }; - /** Update all references of a property of a data object. */ + /** Replace **all** references in cross-reference property of an object. */ 'objects.class.references.put': { parameters: { path: { @@ -2006,7 +2554,7 @@ export interface operations { }; }; }; - /** Add a single reference to a class-property. */ + /** Add a single reference to an object. This adds a reference to the array of cross-references of the given property in the source object specified by its collection name and id */ 'objects.class.references.create': { parameters: { path: { @@ -2052,7 +2600,7 @@ export interface operations { }; }; }; - /** Delete the single reference that is given in the body from the list of references that this property of a data object has */ + /** Delete the single reference that is given in the body from the list of references that this property has. */ 'objects.class.references.delete': { parameters: { path: { @@ -2100,7 +2648,7 @@ export interface operations { }; }; }; - /** Validate an Object's schema and meta-data. It has to be based on a schema, which is related to the given Object to be accepted by this validation. */ + /** Validate an object's schema and meta-data without creating it.

If the schema of the object is valid, the request should return nothing with a plain RESTful request. Otherwise, an error object will be returned. */ 'objects.validate': { parameters: { body: { @@ -2126,7 +2674,7 @@ export interface operations { }; }; }; - /** Register new Objects in bulk. Provided meta-data and schema values are validated. */ + /** Create new objects in bulk.

Meta-data and schema values are validated.

**Note: idempotence of `/batch/objects`**:
`POST /batch/objects` is idempotent, and will overwrite any existing object given the same id. */ 'batch.objects.create': { parameters: { body: { @@ -2166,7 +2714,7 @@ export interface operations { }; }; }; - /** Delete Objects in bulk that match a certain filter. */ + /** Batch delete objects that match a particular filter.

The request body takes a single `where` filter and will delete all objects matched.

Note that there is a limit to the number of objects to be deleted at once using this filter, in order to protect against unexpected memory surges and very-long-running requests. The default limit is 10,000 and may be configured by setting the `QUERY_MAXIMUM_RESULTS` environment variable.

Objects are deleted in the same order that they would be returned in an equivalent Get query. To delete more objects than the limit, run the same query multiple times. */ 'batch.objects.delete': { parameters: { body: { @@ -2204,7 +2752,7 @@ export interface operations { }; }; }; - /** Register cross-references between any class items (objects or objects) in bulk. */ + /** Batch create cross-references between collections items (objects or objects) in bulk. */ 'batch.references.create': { parameters: { body: { @@ -2241,7 +2789,7 @@ export interface operations { }; }; }; - /** Get an object based on GraphQL */ + /** Get a response based on a GraphQL query */ 'graphql.post': { parameters: { body: { @@ -2299,7 +2847,7 @@ export interface operations { }; }; }; - /** Gives meta information about the server and can be used to provide information to another Weaviate instance that wants to interact with the current instance. */ + /** Returns meta information about the server. Can be used to provide information to another Weaviate instance that wants to interact with the current instance. */ 'meta.get': { responses: { /** Successful response. */ @@ -2318,6 +2866,7 @@ export interface operations { }; }; }; + /** Fetch an array of all collection definitions from the schema. */ 'schema.dump': { parameters: { header: { @@ -2342,6 +2891,7 @@ export interface operations { }; }; }; + /** Create a new data object collection.

If AutoSchema is enabled, Weaviate will attempt to infer the schema from the data at import time. However, manual schema definition is recommended for production environments. */ 'schema.objects.create': { parameters: { body: { @@ -2398,7 +2948,7 @@ export interface operations { }; }; }; - /** Use this endpoint to alter an existing class in the schema. Note that not all settings are mutable. If an error about immutable fields is returned and you still need to update this particular setting, you will have to delete the class (and the underlying data) and recreate. This endpoint cannot be used to modify properties. Instead use POST /v1/schema/{className}/properties. A typical use case for this endpoint is to update configuration, such as the vectorIndexConfig. Note that even in mutable sections, such as vectorIndexConfig, some fields may be immutable. */ + /** Add a property to an existing collection. */ 'schema.objects.update': { parameters: { path: { @@ -2433,6 +2983,7 @@ export interface operations { }; }; }; + /** Remove a collection from the schema. This will also delete all the objects in the collection. */ 'schema.objects.delete': { parameters: { path: { @@ -2488,6 +3039,7 @@ export interface operations { }; }; }; + /** Get the status of every shard in the cluster. */ 'schema.objects.shards.get': { parameters: { path: { @@ -2518,7 +3070,7 @@ export interface operations { }; }; }; - /** Update shard status of an Object Class */ + /** Update a shard status for a collection. For example, a shard may have been marked as `READONLY` because its disk was full. After providing more disk space, use this endpoint to set the shard status to `READY` again. There is also a convenience function in each client to set the status of all shards of a collection. */ 'schema.objects.shards.update': { parameters: { path: { @@ -2617,7 +3169,7 @@ export interface operations { }; }; }; - /** Create a new tenant for a specific class */ + /** Create a new tenant for a collection. Multi-tenancy must be enabled in the collection definition. */ 'tenants.create': { parameters: { path: { @@ -2677,6 +3229,41 @@ export interface operations { }; }; }; + /** get a specific tenant for the given class */ + 'tenants.get.one': { + parameters: { + path: { + className: string; + tenantName: string; + }; + header: { + /** If consistency is true, the request will be proxied to the leader to ensure strong schema consistency */ + consistency?: boolean; + }; + }; + responses: { + /** load the tenant given the specified class */ + 200: { + schema: definitions['TenantResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Tenant not found */ + 404: unknown; + /** Invalid tenant or class */ + 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']; + }; + }; + }; /** Check if a tenant exists for a specific class */ 'tenant.exists': { parameters: { @@ -2739,11 +3326,11 @@ export interface operations { }; }; }; - /** Starts a process of creating a backup for a set of classes */ + /** Start creating a backup for a set of collections.

Notes:
- Weaviate uses gzip compression by default.
- Weaviate stays usable while a backup process is ongoing. */ 'backups.create': { parameters: { path: { - /** Backup backend name e.g. filesystem, gcs, s3. */ + /** Backup backend name e.g. `filesystem`, `gcs`, `s3`, `azure`. */ backend: string; }; body: { @@ -2771,7 +3358,7 @@ export interface operations { }; }; }; - /** Returns status of backup creation attempt for a set of classes */ + /** Returns status of backup creation attempt for a set of collections.

All client implementations have a `wait for completion` option which will poll the backup status in the background and only return once the backup has completed (successfully or unsuccessfully). If you set the `wait for completion` option to false, you can also check the status yourself using this endpoint. */ 'backups.create.status': { parameters: { path: { @@ -2780,6 +3367,12 @@ export interface operations { /** The ID of a backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id: string; }; + query: { + /** Name of the bucket, container, volume, etc */ + bucket?: string; + /** The path within the bucket */ + path?: string; + }; }; responses: { /** Backup creation status successfully returned */ @@ -2815,6 +3408,12 @@ export interface operations { /** The ID of a backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id: string; }; + query: { + /** Name of the bucket, container, volume, etc */ + bucket?: string; + /** The path within the bucket */ + path?: string; + }; }; responses: { /** Successfully deleted. */ @@ -2835,15 +3434,21 @@ export interface operations { }; }; }; - /** Returns status of a backup restoration attempt for a set of classes */ + /** Returns status of a backup restoration attempt for a set of classes.

All client implementations have a `wait for completion` option which will poll the backup status in the background and only return once the backup has completed (successfully or unsuccessfully). If you set the `wait for completion` option to false, you can also check the status yourself using the this endpoint. */ 'backups.restore.status': { parameters: { path: { - /** Backup backend name e.g. filesystem, gcs, s3. */ + /** Backup backend name e.g. `filesystem`, `gcs`, `s3`, `azure`. */ backend: string; /** The ID of a backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id: string; }; + query: { + /** Name of the bucket, container, volume, etc */ + bucket?: string; + /** The path within the bucket */ + path?: string; + }; }; responses: { /** Backup restoration status successfully returned */ @@ -2866,11 +3471,11 @@ export interface operations { }; }; }; - /** Starts a process of restoring a backup for a set of classes */ + /** Starts a process of restoring a backup for a set of collections.

Any backup can be restored to any machine, as long as the number of nodes between source and target are identical.

Requrements:

- None of the collections to be restored already exist on the target restoration node(s).
- The node names of the backed-up collections' must match those of the target restoration node(s). */ 'backups.restore': { parameters: { path: { - /** Backup backend name e.g. filesystem, gcs, s3. */ + /** Backup backend name e.g. `filesystem`, `gcs`, `s3`, `azure`. */ backend: string; /** The ID of a backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id: string; @@ -2927,7 +3532,7 @@ export interface operations { }; }; }; - /** Returns status of Weaviate DB. */ + /** Returns node information for the entire database. */ 'nodes.get': { parameters: { query: { @@ -2960,7 +3565,7 @@ export interface operations { }; }; }; - /** Returns status of Weaviate DB. */ + /** Returns node information for the nodes relevant to the collection. */ 'nodes.get.class': { parameters: { path: { diff --git a/src/openapi/types.ts b/src/openapi/types.ts index b1b27824..44b7d0d8 100644 --- a/src/openapi/types.ts +++ b/src/openapi/types.ts @@ -64,3 +64,7 @@ export type BatchStats = definitions['BatchStats']; export type NodeShardStatus = definitions['NodeShardStatus']; // Meta export type Meta = definitions['Meta']; +// RBAC +export type Role = definitions['Role']; +export type Permission = definitions['Permission']; +export type Action = definitions['Permission']['action']; diff --git a/src/roles/index.ts b/src/roles/index.ts new file mode 100644 index 00000000..0bf17846 --- /dev/null +++ b/src/roles/index.ts @@ -0,0 +1,174 @@ +import { ConnectionREST } from '../index.js'; +import { Permission as WeaviatePermission, Role as WeaviateRole } from '../openapi/types.js'; +import { + BackupsPermission, + ClusterPermission, + CollectionsPermission, + DataPermission, + NodesPermission, + Permission, + PermissionsInput, + Role, + RolesPermission, + User, +} from './types.js'; +import { Map } from './util.js'; + +export interface Roles { + listAll: () => Promise>; + ofCurrentUser: () => Promise>; + byName: (roleName: string) => Promise; + byUser: (user: string) => Promise>; + assignedUsers: (roleName: string) => Promise>; + delete: (roleName: string) => Promise; + create: (roleName: string, permissions: PermissionsInput) => Promise; + assignToUser: (roleNames: string | string[], user: string) => Promise; + exists: (roleName: string) => Promise; + revokeFromUser: (roleNames: string | string[], user: string) => Promise; + addPermissions: (roleName: string, permissions: PermissionsInput) => Promise; + removePermissions: (roleName: string, permissions: PermissionsInput) => Promise; + hasPermission: (roleName: string, 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), + create: (roleName: string, permissions: PermissionsInput) => { + const perms = Map.flattenPermissions(permissions).map(Map.permissionToWeaviate); + return connection + .postEmpty('/authz/roles', { + name: roleName, + permissions: perms, + }) + .then(() => Map.roleFromWeaviate({ name: roleName, permissions: perms })); + }, + delete: (roleName: string) => connection.delete(`/authz/roles/${roleName}`, null), + exists: (roleName: string) => + connection + .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) + ), + }; +}; + +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' }); + } + return out; + }); + }, + cluster: (args: { read?: boolean }): ClusterPermission[] => { + const out: ClusterPermission[] = []; + if (args.read) { + out.push({ action: 'read_cluster' }); + } + return out; + }, + collections: (args: { + collection: string | string[]; + create_collection?: boolean; + read_config?: boolean; + update_config?: boolean; + delete_collection?: boolean; + }): 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' }); + } + return out; + }); + }, + data: (args: { + collection: 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[] = []; + 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' }); + } + 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) => { + const out: NodesPermission[] = []; + if (args.read) { + out.push({ collection, action: 'read_nodes', verbosity: args.verbosity || 'verbose' }); + } + 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' }); + } + return out; + }); + }, +}; + +export default roles; diff --git a/src/roles/integration.test.ts b/src/roles/integration.test.ts new file mode 100644 index 00000000..fd9355e2 --- /dev/null +++ b/src/roles/integration.test.ts @@ -0,0 +1,207 @@ +import { StartedWeaviateContainer, WeaviateContainer } from '@testcontainers/weaviate'; +import weaviate, { ApiKey, Permission, Role, WeaviateClient } from '..'; +import { WeaviateInsufficientPermissionsError, WeaviateUnexpectedStatusCodeError } from '../errors'; +import { DbVersion } from '../utils/dbVersion'; + +const only = DbVersion.fromString(`v${process.env.WEAVIATE_VERSION!}`).isAtLeast(1, 28, 0) + ? describe + : describe.skip; + +only('Integration testing of the roles namespace', () => { + let client: WeaviateClient; + let container: StartedWeaviateContainer; + + beforeAll(async () => { + container = await new WeaviateContainer(`semitechnologies/weaviate:${process.env.WEAVIATE_VERSION}`) + .withExposedPorts(8080, 50051) + .withEnvironment({ + AUTHENTICATION_APIKEY_ENABLED: 'true', + AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'admin-key,custom-key', + AUTHENTICATION_APIKEY_USERS: 'admin-user,custom-user', + AUTHORIZATION_ADMIN_USERS: 'admin-user', + AUTHORIZATION_ENABLE_RBAC: 'true', + }) + .start(); + expect(container).toBeDefined(); + client = await weaviate.connectToLocal({ + host: container.getHost(), + port: container.getMappedPort(8080), + grpcPort: container.getMappedPort(50051), + authCredentials: new ApiKey('admin-key'), + }); + }); + + afterAll(async () => { + await container.stop(); + }); + + it('should be able to retrieve the default roles', async () => { + const roles = await client.roles.listAll(); + expect(Object.values(roles).length).toBeGreaterThan(0); + }); + + it('should fail with insufficient permissions if no key provided', async () => { + const unauthenticatedClient = await weaviate.connectToLocal({ + host: container.getHost(), + port: container.getMappedPort(8080), + grpcPort: container.getMappedPort(50051), + }); + await expect(unauthenticatedClient.roles.listAll()).rejects.toThrowError( + WeaviateInsufficientPermissionsError + ); // should be unauthenticated error, needs fixing on server + }); + + it('should fail with insufficient permissions if permission-less key provided', async () => { + const unauthenticatedClient = await weaviate.connectToLocal({ + host: container.getHost(), + port: container.getMappedPort(8080), + grpcPort: container.getMappedPort(50051), + authCredentials: new ApiKey('custom-key'), + }); + await expect(unauthenticatedClient.roles.listAll()).rejects.toThrowError( + WeaviateInsufficientPermissionsError + ); + }); + + 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(); + }); + + it('should check the existance of a fake role', async () => { + const exists = await client.roles.exists('fake-role'); + expect(exists).toBeFalsy(); + }); + + 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', action: 'manage_backups' }], + clusterPermissions: [], + collectionsPermissions: [], + dataPermissions: [], + nodesPermissions: [], + rolesPermissions: [], + }, + }, + { + roleName: 'cluster', + permissions: weaviate.permissions.cluster({ read: true }), + expected: { + name: 'cluster', + backupsPermissions: [], + clusterPermissions: [{ action: 'read_cluster' }], + collectionsPermissions: [], + dataPermissions: [], + nodesPermissions: [], + rolesPermissions: [], + }, + }, + { + 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', action: 'create_collections' }, + { collection: 'Some-collection', action: 'read_collections' }, + { collection: 'Some-collection', action: 'update_collections' }, + { collection: 'Some-collection', action: 'delete_collections' }, + ], + dataPermissions: [], + nodesPermissions: [], + rolesPermissions: [], + }, + }, + { + 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', action: 'create_data' }, + { collection: 'Some-collection', action: 'read_data' }, + { collection: 'Some-collection', action: 'update_data' }, + { collection: 'Some-collection', action: 'delete_data' }, + ], + nodesPermissions: [], + rolesPermissions: [], + }, + }, + { + roleName: 'nodes', + permissions: weaviate.permissions.nodes({ + collection: 'Some-collection', + verbosity: 'verbose', + read: true, + }), + expected: { + name: 'nodes', + backupsPermissions: [], + clusterPermissions: [], + collectionsPermissions: [], + dataPermissions: [], + nodesPermissions: [{ collection: 'Some-collection', verbosity: 'verbose', action: 'read_nodes' }], + rolesPermissions: [], + }, + }, + { + roleName: 'roles', + permissions: weaviate.permissions.roles({ role: 'some-role', manage: true }), + expected: { + name: 'roles', + backupsPermissions: [], + clusterPermissions: [], + collectionsPermissions: [], + dataPermissions: [], + nodesPermissions: [], + rolesPermissions: [{ role: 'some-role', action: 'manage_roles' }], + }, + }, + ]; + testCases.forEach((testCase) => { + it(`with ${testCase.roleName} permissions`, async () => { + await client.roles.create(testCase.roleName, testCase.permissions); + const role = await client.roles.byName(testCase.roleName); + expect(role).toEqual(testCase.expected); + }); + }); + }); + + it('should delete one of the created roles', async () => { + await client.roles.delete('backups'); + await expect(client.roles.byName('backups')).rejects.toThrowError(WeaviateUnexpectedStatusCodeError); + await expect(client.roles.exists('backups')).resolves.toBeFalsy(); + }); +}); diff --git a/src/roles/types.ts b/src/roles/types.ts new file mode 100644 index 00000000..bb11d17e --- /dev/null +++ b/src/roles/types.ts @@ -0,0 +1,72 @@ +import { Action } from '../openapi/types.js'; + +export type BackupsAction = Extract; +export type ClusterAction = Extract; +export type CollectionsAction = Extract< + Action, + | 'create_collections' + | 'delete_collections' + | 'read_collections' + | 'update_collections' + | 'manage_collections' +>; +export type DataAction = Extract< + Action, + 'create_data' | 'delete_data' | 'read_data' | 'update_data' | 'manage_data' +>; +export type NodesAction = Extract; +export type RolesAction = Extract; + +export type BackupsPermission = { + collection: string; + action: BackupsAction; +}; + +export type ClusterPermission = { + action: ClusterAction; +}; + +export type CollectionsPermission = { + collection: string; + action: CollectionsAction; +}; + +export type DataPermission = { + collection: string; + action: DataAction; +}; + +export type NodesPermission = { + collection: string; + verbosity: 'verbose' | 'minimal'; + action: NodesAction; +}; + +export type RolesPermission = { + role: string; + action: RolesAction; +}; + +export type Role = { + name: string; + backupsPermissions: BackupsPermission[]; + clusterPermissions: ClusterPermission[]; + collectionsPermissions: CollectionsPermission[]; + dataPermissions: DataPermission[]; + nodesPermissions: NodesPermission[]; + rolesPermissions: RolesPermission[]; +}; + +export type User = { + name: string; +}; + +export type Permission = + | BackupsPermission + | ClusterPermission + | CollectionsPermission + | DataPermission + | NodesPermission + | RolesPermission; + +export type PermissionsInput = Permission | Permission[] | Permission[][] | (Permission | Permission[])[]; diff --git a/src/roles/util.ts b/src/roles/util.ts new file mode 100644 index 00000000..299579f5 --- /dev/null +++ b/src/roles/util.ts @@ -0,0 +1,155 @@ +import { Permission as WeaviatePermission, Role as WeaviateRole } from '../openapi/types.js'; +import { + BackupsAction, + BackupsPermission, + ClusterPermission, + CollectionsAction, + CollectionsPermission, + DataAction, + DataPermission, + NodesAction, + NodesPermission, + Permission, + PermissionsInput, + Role, + RolesAction, + RolesPermission, + User, +} from './types.js'; + +export class PermissionGuards { + static isBackups = (permission: Permission): permission is BackupsPermission => + (permission as BackupsPermission).action === 'manage_backups'; + static isCluster = (permission: Permission): permission is ClusterPermission => + (permission as ClusterPermission).action === 'read_cluster'; + static isCollections = (permission: Permission): permission is CollectionsPermission => + [ + 'create_collections', + 'delete_collections', + 'read_collections', + 'update_collections', + 'manage_collections', + ].includes((permission as CollectionsPermission).action); + static isData = (permission: Permission): permission is DataPermission => + ['create_data', 'delete_data', 'read_data', 'update_data', 'manage_data'].includes( + (permission as DataPermission).action + ); + static isNodes = (permission: Permission): permission is NodesPermission => + (permission as NodesPermission).action === 'read_nodes'; + static isRoles = (permission: Permission): permission is RolesPermission => + (permission as RolesPermission).action === 'manage_roles'; + static isPermission = (permissions: PermissionsInput): permissions is Permission => + !Array.isArray(permissions); + static isPermissionArray = (permissions: PermissionsInput): permissions is Permission[] => + Array.isArray(permissions) && permissions.every(PermissionGuards.isPermission); + static isPermissionMatrix = (permissions: PermissionsInput): permissions is Permission[][] => + Array.isArray(permissions) && permissions.every(PermissionGuards.isPermissionArray); + static isPermissionTuple = (permissions: PermissionsInput): permissions is (Permission | Permission[])[] => + Array.isArray(permissions) && + permissions.every( + (permission) => + PermissionGuards.isPermission(permission) || PermissionGuards.isPermissionArray(permission) + ); +} + +export class Map { + static flattenPermissions = (permissions: PermissionsInput): Permission[] => + !Array.isArray(permissions) ? [permissions] : permissions.flat(2); + + static permissionToWeaviate = (permission: Permission): WeaviatePermission => { + if (PermissionGuards.isBackups(permission)) { + return { backups: { collection: permission.collection }, action: permission.action }; + } else if (PermissionGuards.isCluster(permission)) { + return { action: permission.action }; + } else if (PermissionGuards.isCollections(permission)) { + return { collections: { collection: permission.collection }, action: permission.action }; + } else if (PermissionGuards.isData(permission)) { + return { data: { collection: permission.collection }, action: permission.action }; + } else if (PermissionGuards.isNodes(permission)) { + return { + nodes: { collection: permission.collection, verbosity: permission.verbosity }, + action: permission.action, + }; + } else if (PermissionGuards.isRoles(permission)) { + return { roles: { role: permission.role }, action: permission.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: [], + }; + 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, + }); + } else if (permission.action === 'read_cluster') { + out.clusterPermissions.push({ + action: permission.action, + }); + } 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, + }); + } 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, + }); + } 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, + }); + } 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, + }); + } + }); + return out; + }; + + static roles = (roles: WeaviateRole[]): Record => + roles.reduce((acc, role) => { + acc[role.name] = Map.roleFromWeaviate(role); + return acc; + }, {} as Record); + + static users = (users: string[]): Record => + users.reduce((acc, user) => { + acc[user] = { name: user }; + return acc; + }, {} as Record); +} diff --git a/src/schema/classExists.ts b/src/schema/classExists.ts index 17ee5755..114e440d 100644 --- a/src/schema/classExists.ts +++ b/src/schema/classExists.ts @@ -32,7 +32,7 @@ export default class ClassExists extends CommandBase { } const path = `/schema`; return this.client - .get(path) - .then((res: WeaviateSchema) => res.classes?.some((c) => c.class === this.className)); + .get(path) + .then((res) => (res.classes ? res.classes.some((c) => c.class === this.className) : false)); }; } diff --git a/src/schema/shardsGetter.ts b/src/schema/shardsGetter.ts index 13b52343..bce06794 100644 --- a/src/schema/shardsGetter.ts +++ b/src/schema/shardsGetter.ts @@ -43,5 +43,5 @@ export default class ShardsGetter extends CommandBase { export function getShards(client: Connection, className: any, tenant?: string) { const path = `/schema/${className}/shards${tenant ? `?tenant=${tenant}` : ''}`; - return client.get(path); + return client.get(path); }