From de970d9996a7fa4dcfcd6b9e3f9d39c5256be255 Mon Sep 17 00:00:00 2001 From: Peter Amstutz Date: Tue, 22 Apr 2025 17:27:17 -0400 Subject: [PATCH] 22680: Passing API server tests Arvados-DCO-1.1-Signed-off-by: Peter Amstutz --- sdk/R/arvados-v1-discovery.json | 304 ++++++++++++++++++ sdk/go/arvados/api.go | 7 +- sdk/go/arvados/credential.go | 31 ++ sdk/python/arvados-v1-discovery.json | 304 ++++++++++++++++++ .../arvados/v1/credentials_controller.rb | 6 +- .../arvados/v1/schema_controller.rb | 5 + ...20250422103000_create_credentials_table.rb | 3 + services/api/db/structure.sql | 21 ++ 8 files changed, 679 insertions(+), 2 deletions(-) create mode 100644 sdk/go/arvados/credential.go diff --git a/sdk/R/arvados-v1-discovery.json b/sdk/R/arvados-v1-discovery.json index f76fce840e..2d8ad23429 100644 --- a/sdk/R/arvados-v1-discovery.json +++ b/sdk/R/arvados-v1-discovery.json @@ -1481,6 +1481,235 @@ } } }, + "credentials": { + "methods": { + "get": { + "id": "arvados.credentials.get", + "path": "credentials/{uuid}", + "httpMethod": "GET", + "description": "Get a Credential record by UUID.", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to return.", + "required": true, + "location": "path" + }, + "select": { + "type": "array", + "description": "An array of names of attributes to return in the response.", + "required": false, + "location": "query" + } + }, + "parameterOrder": [ + "uuid" + ], + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados", + "https://api.arvados.org/auth/arvados.readonly" + ] + }, + "list": { + "id": "arvados.credentials.list", + "path": "credentials", + "httpMethod": "GET", + "description": "Retrieve a CredentialList.", + "parameters": { + "filters": { + "type": "array", + "required": false, + "description": "Filters to limit which objects are returned by their attributes.\nRefer to the [filters reference][] for more information about how to write filters.\n\n[filters reference]: https://doc.arvados.org/api/methods.html#filters\n", + "location": "query" + }, + "where": { + "type": "object", + "required": false, + "description": "An object to limit which objects are returned by their attributes.\nThe keys of this object are attribute names.\nEach value is either a single matching value or an array of matching values for that attribute.\nThe `filters` parameter is more flexible and preferred.\n", + "location": "query" + }, + "order": { + "type": "array", + "required": false, + "description": "An array of strings to set the order in which matching objects are returned.\nEach string has the format ` `.\n`DIRECTION` can be `asc` or omitted for ascending, or `desc` for descending.\n", + "location": "query" + }, + "select": { + "type": "array", + "description": "An array of names of attributes to return from each matching object.", + "required": false, + "location": "query" + }, + "distinct": { + "type": "boolean", + "required": false, + "default": "false", + "description": "If this is true, and multiple objects have the same values\nfor the attributes that you specify in the `select` parameter, then each unique\nset of values will only be returned once in the result set.\n", + "location": "query" + }, + "limit": { + "type": "integer", + "required": false, + "default": "100", + "description": "The maximum number of objects to return in the result.\nNote that the API may return fewer results than this if your request hits other\nlimits set by the administrator.\n", + "location": "query" + }, + "offset": { + "type": "integer", + "required": false, + "default": "0", + "description": "Return matching objects starting from this index.\nNote that result indexes may change if objects are modified in between a series\nof list calls.\n", + "location": "query" + }, + "count": { + "type": "string", + "required": false, + "default": "exact", + "description": "A string to determine result counting behavior. Supported values are:\n\n * `\"exact\"`: The response will include an `items_available` field that\n counts the number of objects that matched this search criteria,\n including ones not included in `items`.\n\n * `\"none\"`: The response will not include an `items_avaliable`\n field. This improves performance by returning a result as soon as enough\n `items` have been loaded for this result.\n\n", + "location": "query" + }, + "cluster_id": { + "type": "string", + "description": "Cluster ID of a federated cluster to return objects from", + "location": "query", + "required": false + }, + "bypass_federation": { + "type": "boolean", + "required": false, + "default": "false", + "description": "If true, do not return results from other clusters in the\nfederation, only the cluster that received the request.\nYou must be an administrator to use this flag.\n", + "location": "query" + } + }, + "response": { + "$ref": "CredentialList" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados", + "https://api.arvados.org/auth/arvados.readonly" + ] + }, + "create": { + "id": "arvados.credentials.create", + "path": "credentials", + "httpMethod": "POST", + "description": "Create a new Credential.", + "parameters": { + "select": { + "type": "array", + "description": "An array of names of attributes to return in the response.", + "required": false, + "location": "query" + }, + "ensure_unique_name": { + "type": "boolean", + "description": "If the given name is already used by this owner, adjust the name to ensure uniqueness instead of returning an error.", + "location": "query", + "required": false, + "default": "false" + }, + "cluster_id": { + "type": "string", + "description": "Cluster ID of a federated cluster where this object should be created.", + "location": "query", + "required": false + } + }, + "request": { + "required": true, + "properties": { + "credential": { + "$ref": "Credential" + } + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + }, + "update": { + "id": "arvados.credentials.update", + "path": "credentials/{uuid}", + "httpMethod": "PUT", + "description": "Update attributes of an existing Credential.", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to update.", + "required": true, + "location": "path" + }, + "select": { + "type": "array", + "description": "An array of names of attributes to return in the response.", + "required": false, + "location": "query" + } + }, + "request": { + "required": true, + "properties": { + "credential": { + "$ref": "Credential" + } + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + }, + "delete": { + "id": "arvados.credentials.delete", + "path": "credentials/{uuid}", + "httpMethod": "DELETE", + "description": "Delete an existing Credential.", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to delete.", + "required": true, + "location": "path" + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + }, + "credential_secret": { + "id": "arvados.credentials.credential_secret", + "path": "credentials/{uuid}/credential_secret", + "httpMethod": "GET", + "description": "Fetch the secret part of the credential (can only be invoked by running containers).", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to query.", + "required": true, + "location": "path" + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + } + } + }, "groups": { "methods": { "get": { @@ -4411,6 +4640,81 @@ } } }, + "CredentialList": { + "id": "CredentialList", + "description": "A list of Credential objects.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "description": "Object type. Always arvados#credentialList.", + "default": "arvados#credentialList" + }, + "etag": { + "type": "string", + "description": "List cache version." + }, + "items": { + "type": "array", + "description": "An array of matching Credential objects.", + "items": { + "$ref": "Credential" + } + } + } + }, + "Credential": { + "id": "Credential", + "description": "Arvados credential.", + "type": "object", + "uuidPrefix": "oss07", + "properties": { + "etag": { + "type": "string", + "description": "Object cache version." + }, + "uuid": { + "type": "string", + "description": "This credential's Arvados UUID, like `zzzzz-oss07-12345abcde67890`." + }, + "owner_uuid": { + "description": "The UUID of the user or group that owns this credential.", + "type": "string" + }, + "created_at": { + "description": "The time this credential was created. The string encodes a UTC date and time in ISO 8601 format.", + "type": "datetime" + }, + "modified_at": { + "description": "The time this credential was last updated. The string encodes a UTC date and time in ISO 8601 format.", + "type": "datetime" + }, + "modified_by_user_uuid": { + "description": "The UUID of the user that last updated this credential.", + "type": "string" + }, + "name": { + "description": "The name of this credential assigned by a user.", + "type": "string" + }, + "description": { + "description": "A longer HTML description of this credential assigned by a user.\nAllowed HTML tags are `a`, `b`, `blockquote`, `br`, `code`,\n`del`, `dd`, `dl`, `dt`, `em`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `hr`,\n`i`, `img`, `kbd`, `li`, `ol`, `p`, `pre`,\n`s`, `section`, `span`, `strong`, `sub`, `sup`, and `ul`.", + "type": "text" + }, + "credential_class": { + "description": "The type of credential being stored.", + "type": "string" + }, + "credential_id": { + "description": "The non-secret part of the credential, e.g. a username.", + "type": "string" + }, + "expires_at": { + "description": "Date after which the credential_secret field is no longer valid. The string encodes a UTC date and time in ISO 8601 format.", + "type": "datetime" + } + } + }, "GroupList": { "id": "GroupList", "description": "A list of Group objects.", diff --git a/sdk/go/arvados/api.go b/sdk/go/arvados/api.go index 6180d168b3..3071d0cbc2 100644 --- a/sdk/go/arvados/api.go +++ b/sdk/go/arvados/api.go @@ -104,7 +104,12 @@ var ( EndpointAPIClientAuthorizationUpdate = APIEndpoint{"PUT", "arvados/v1/api_client_authorizations/{uuid}", "api_client_authorization"} EndpointAPIClientAuthorizationList = APIEndpoint{"GET", "arvados/v1/api_client_authorizations", ""} EndpointAPIClientAuthorizationDelete = APIEndpoint{"DELETE", "arvados/v1/api_client_authorizations/{uuid}", ""} - EndpointAPIClientAuthorizationGet = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/{uuid}", ""} + EndpointAPIClientAuthorizationGet = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/{uuid}", ""}, + EndpointCredentialCreate = APIEndpoint{"POST", "arvados/v1/credentials", "credential"}, + EndpointCredentialUpdate = APIEndpoint{"PATCH", "arvados/v1/credentials/{uuid}", "credential"}, + EndpointCredentialGet = APIEndpoint{"GET", "arvados/v1/credentials/{uuid}", ""}, + EndpointCredentialDelete = APIEndpoint{"DELETE", "arvados/v1/credentials/{uuid}", ""}, + EndpointCredentialSecret = APIEndpoint{"GET", "arvados/v1/credentials/{uuid}/credential_secret", ""}, ) type ContainerHTTPProxyOptions struct { diff --git a/sdk/go/arvados/credential.go b/sdk/go/arvados/credential.go new file mode 100644 index 0000000000..93d10824d5 --- /dev/null +++ b/sdk/go/arvados/credential.go @@ -0,0 +1,31 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package arvados + +import "time" + +// Credential is an arvados#credential record +type Credential struct { + UUID string `json:"uuid,omitempty"` + Etag string `json:"etag"` + OwnerUUID string `json:"owner_uuid"` + CreatedAt time.Time `json:"created_at"` + ModifiedAt time.Time `json:"modified_at"` + ModifiedByUserUUID string `json:"modified_by_user_uuid"` + Name string `json:"name"` + Description string `json:"description"` + CredentialClass string `json:"credential_class"` + CredentialId string `json:"credential_id"` + CredentialSecret string `json:"credential_secret,omitempty"` + ExpiresAt time.Time `json:"expires_at"` +} + +// CredentialList is an arvados#credentialList resource. +type CredentialList struct { + Items []Credential `json:"items"` + ItemsAvailable int `json:"items_available"` + Offset int `json:"offset"` + Limit int `json:"limit"` +} diff --git a/sdk/python/arvados-v1-discovery.json b/sdk/python/arvados-v1-discovery.json index f76fce840e..2d8ad23429 100644 --- a/sdk/python/arvados-v1-discovery.json +++ b/sdk/python/arvados-v1-discovery.json @@ -1481,6 +1481,235 @@ } } }, + "credentials": { + "methods": { + "get": { + "id": "arvados.credentials.get", + "path": "credentials/{uuid}", + "httpMethod": "GET", + "description": "Get a Credential record by UUID.", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to return.", + "required": true, + "location": "path" + }, + "select": { + "type": "array", + "description": "An array of names of attributes to return in the response.", + "required": false, + "location": "query" + } + }, + "parameterOrder": [ + "uuid" + ], + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados", + "https://api.arvados.org/auth/arvados.readonly" + ] + }, + "list": { + "id": "arvados.credentials.list", + "path": "credentials", + "httpMethod": "GET", + "description": "Retrieve a CredentialList.", + "parameters": { + "filters": { + "type": "array", + "required": false, + "description": "Filters to limit which objects are returned by their attributes.\nRefer to the [filters reference][] for more information about how to write filters.\n\n[filters reference]: https://doc.arvados.org/api/methods.html#filters\n", + "location": "query" + }, + "where": { + "type": "object", + "required": false, + "description": "An object to limit which objects are returned by their attributes.\nThe keys of this object are attribute names.\nEach value is either a single matching value or an array of matching values for that attribute.\nThe `filters` parameter is more flexible and preferred.\n", + "location": "query" + }, + "order": { + "type": "array", + "required": false, + "description": "An array of strings to set the order in which matching objects are returned.\nEach string has the format ` `.\n`DIRECTION` can be `asc` or omitted for ascending, or `desc` for descending.\n", + "location": "query" + }, + "select": { + "type": "array", + "description": "An array of names of attributes to return from each matching object.", + "required": false, + "location": "query" + }, + "distinct": { + "type": "boolean", + "required": false, + "default": "false", + "description": "If this is true, and multiple objects have the same values\nfor the attributes that you specify in the `select` parameter, then each unique\nset of values will only be returned once in the result set.\n", + "location": "query" + }, + "limit": { + "type": "integer", + "required": false, + "default": "100", + "description": "The maximum number of objects to return in the result.\nNote that the API may return fewer results than this if your request hits other\nlimits set by the administrator.\n", + "location": "query" + }, + "offset": { + "type": "integer", + "required": false, + "default": "0", + "description": "Return matching objects starting from this index.\nNote that result indexes may change if objects are modified in between a series\nof list calls.\n", + "location": "query" + }, + "count": { + "type": "string", + "required": false, + "default": "exact", + "description": "A string to determine result counting behavior. Supported values are:\n\n * `\"exact\"`: The response will include an `items_available` field that\n counts the number of objects that matched this search criteria,\n including ones not included in `items`.\n\n * `\"none\"`: The response will not include an `items_avaliable`\n field. This improves performance by returning a result as soon as enough\n `items` have been loaded for this result.\n\n", + "location": "query" + }, + "cluster_id": { + "type": "string", + "description": "Cluster ID of a federated cluster to return objects from", + "location": "query", + "required": false + }, + "bypass_federation": { + "type": "boolean", + "required": false, + "default": "false", + "description": "If true, do not return results from other clusters in the\nfederation, only the cluster that received the request.\nYou must be an administrator to use this flag.\n", + "location": "query" + } + }, + "response": { + "$ref": "CredentialList" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados", + "https://api.arvados.org/auth/arvados.readonly" + ] + }, + "create": { + "id": "arvados.credentials.create", + "path": "credentials", + "httpMethod": "POST", + "description": "Create a new Credential.", + "parameters": { + "select": { + "type": "array", + "description": "An array of names of attributes to return in the response.", + "required": false, + "location": "query" + }, + "ensure_unique_name": { + "type": "boolean", + "description": "If the given name is already used by this owner, adjust the name to ensure uniqueness instead of returning an error.", + "location": "query", + "required": false, + "default": "false" + }, + "cluster_id": { + "type": "string", + "description": "Cluster ID of a federated cluster where this object should be created.", + "location": "query", + "required": false + } + }, + "request": { + "required": true, + "properties": { + "credential": { + "$ref": "Credential" + } + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + }, + "update": { + "id": "arvados.credentials.update", + "path": "credentials/{uuid}", + "httpMethod": "PUT", + "description": "Update attributes of an existing Credential.", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to update.", + "required": true, + "location": "path" + }, + "select": { + "type": "array", + "description": "An array of names of attributes to return in the response.", + "required": false, + "location": "query" + } + }, + "request": { + "required": true, + "properties": { + "credential": { + "$ref": "Credential" + } + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + }, + "delete": { + "id": "arvados.credentials.delete", + "path": "credentials/{uuid}", + "httpMethod": "DELETE", + "description": "Delete an existing Credential.", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to delete.", + "required": true, + "location": "path" + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + }, + "credential_secret": { + "id": "arvados.credentials.credential_secret", + "path": "credentials/{uuid}/credential_secret", + "httpMethod": "GET", + "description": "Fetch the secret part of the credential (can only be invoked by running containers).", + "parameters": { + "uuid": { + "type": "string", + "description": "The UUID of the Credential to query.", + "required": true, + "location": "path" + } + }, + "response": { + "$ref": "Credential" + }, + "scopes": [ + "https://api.arvados.org/auth/arvados" + ] + } + } + }, "groups": { "methods": { "get": { @@ -4411,6 +4640,81 @@ } } }, + "CredentialList": { + "id": "CredentialList", + "description": "A list of Credential objects.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "description": "Object type. Always arvados#credentialList.", + "default": "arvados#credentialList" + }, + "etag": { + "type": "string", + "description": "List cache version." + }, + "items": { + "type": "array", + "description": "An array of matching Credential objects.", + "items": { + "$ref": "Credential" + } + } + } + }, + "Credential": { + "id": "Credential", + "description": "Arvados credential.", + "type": "object", + "uuidPrefix": "oss07", + "properties": { + "etag": { + "type": "string", + "description": "Object cache version." + }, + "uuid": { + "type": "string", + "description": "This credential's Arvados UUID, like `zzzzz-oss07-12345abcde67890`." + }, + "owner_uuid": { + "description": "The UUID of the user or group that owns this credential.", + "type": "string" + }, + "created_at": { + "description": "The time this credential was created. The string encodes a UTC date and time in ISO 8601 format.", + "type": "datetime" + }, + "modified_at": { + "description": "The time this credential was last updated. The string encodes a UTC date and time in ISO 8601 format.", + "type": "datetime" + }, + "modified_by_user_uuid": { + "description": "The UUID of the user that last updated this credential.", + "type": "string" + }, + "name": { + "description": "The name of this credential assigned by a user.", + "type": "string" + }, + "description": { + "description": "A longer HTML description of this credential assigned by a user.\nAllowed HTML tags are `a`, `b`, `blockquote`, `br`, `code`,\n`del`, `dd`, `dl`, `dt`, `em`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `hr`,\n`i`, `img`, `kbd`, `li`, `ol`, `p`, `pre`,\n`s`, `section`, `span`, `strong`, `sub`, `sup`, and `ul`.", + "type": "text" + }, + "credential_class": { + "description": "The type of credential being stored.", + "type": "string" + }, + "credential_id": { + "description": "The non-secret part of the credential, e.g. a username.", + "type": "string" + }, + "expires_at": { + "description": "Date after which the credential_secret field is no longer valid. The string encodes a UTC date and time in ISO 8601 format.", + "type": "datetime" + } + } + }, "GroupList": { "id": "GroupList", "description": "A list of Group objects.", diff --git a/services/api/app/controllers/arvados/v1/credentials_controller.rb b/services/api/app/controllers/arvados/v1/credentials_controller.rb index a8b5a6d00d..453a6d3349 100644 --- a/services/api/app/controllers/arvados/v1/credentials_controller.rb +++ b/services/api/app/controllers/arvados/v1/credentials_controller.rb @@ -32,9 +32,13 @@ class Arvados::V1::CredentialsController < ApplicationController end end + def self._credential_secret_method_description + "Fetch the secret part of the credential (can only be invoked by running containers)." + end + def credential_secret c = Container.for_current_token - if @object && c && current_user.can?(read: @object) + if @object && c && c.state == "Running" && current_user.can?(read: @object) if @object.expires_at && Time.now >= @object.expires_at send_error("Credential has expired.", status: 403) else diff --git a/services/api/app/controllers/arvados/v1/schema_controller.rb b/services/api/app/controllers/arvados/v1/schema_controller.rb index 70da465b29..ff32083d6c 100644 --- a/services/api/app/controllers/arvados/v1/schema_controller.rb +++ b/services/api/app/controllers/arvados/v1/schema_controller.rb @@ -519,6 +519,11 @@ cluster, and automatically passes most permissions checks.", "Workflow.definition" => "A string with the CWL source of this %s.", "Workflow.collection_uuid" => "The collection this workflow is linked to, containing the definition of the workflow.", + + "Credential.credential_class" => "The type of credential being stored.", + "Credential.credential_id" => "The non-secret part of the credential, e.g. a username.", + "Credential.credential_secret" => "The secret part of the credential, e.g. a password.", + "Credential.expires_at" => "Date after which the credential_secret field is no longer valid.", } def discovery_doc diff --git a/services/api/db/migrate/20250422103000_create_credentials_table.rb b/services/api/db/migrate/20250422103000_create_credentials_table.rb index 376f889946..0a36c3cf20 100644 --- a/services/api/db/migrate/20250422103000_create_credentials_table.rb +++ b/services/api/db/migrate/20250422103000_create_credentials_table.rb @@ -17,6 +17,9 @@ class CreateCredentialsTable < ActiveRecord::Migration[7.1] t.text :credential_secret t.datetime :expires_at, :null => false end + add_index :credentials, :uuid, unique: true + add_index :credentials, :owner_uuid add_index :credentials, [:owner_uuid, :name], unique: true + add_index :credentials, [:uuid, :owner_uuid, :modified_by_client_uuid, :modified_by_user_uuid, :name, :credential_class, :credential_id] end end diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql index e528357e9d..d08f71f803 100644 --- a/services/api/db/structure.sql +++ b/services/api/db/structure.sql @@ -1960,6 +1960,13 @@ CREATE INDEX groups_trgm_text_search_idx ON public.groups USING gin (((((((((COA CREATE INDEX humans_search_index ON public.humans USING btree (uuid, owner_uuid, modified_by_client_uuid, modified_by_user_uuid); +-- +-- Name: idx_on_uuid_owner_uuid_modified_by_client_uuid_modi_3526840733; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_on_uuid_owner_uuid_modified_by_client_uuid_modi_3526840733 ON public.credentials USING btree (uuid, owner_uuid, modified_by_client_uuid, modified_by_user_uuid, name, credential_class, credential_id); + + -- -- Name: index_api_client_authorizations_on_api_client_id; Type: INDEX; Schema: public; Owner: - -- @@ -2254,6 +2261,13 @@ CREATE INDEX index_containers_on_secret_mounts_md5 ON public.containers USING bt CREATE UNIQUE INDEX index_containers_on_uuid ON public.containers USING btree (uuid); +-- +-- Name: index_credentials_on_owner_uuid; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_credentials_on_owner_uuid ON public.credentials USING btree (owner_uuid); + + -- -- Name: index_credentials_on_owner_uuid_and_name; Type: INDEX; Schema: public; Owner: - -- @@ -2261,6 +2275,13 @@ CREATE UNIQUE INDEX index_containers_on_uuid ON public.containers USING btree (u CREATE UNIQUE INDEX index_credentials_on_owner_uuid_and_name ON public.credentials USING btree (owner_uuid, name); +-- +-- Name: index_credentials_on_uuid; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_credentials_on_uuid ON public.credentials USING btree (uuid); + + -- -- Name: index_frozen_groups_on_uuid; Type: INDEX; Schema: public; Owner: - -- -- 2.39.5