diff --git a/backend/src/ee/services/dynamic-secret/providers/couchbase.ts b/backend/src/ee/services/dynamic-secret/providers/couchbase.ts index 520ce5289..c59f1c2b3 100644 --- a/backend/src/ee/services/dynamic-secret/providers/couchbase.ts +++ b/backend/src/ee/services/dynamic-secret/providers/couchbase.ts @@ -51,6 +51,57 @@ const sanitizeCouchbaseUsername = (username: string): string => { return sanitized; }; +/** + * Normalizes bucket configuration to handle wildcard (*) access consistently. + * + * Key behaviors: + * - If "*" appears anywhere (string or array), grants access to ALL buckets, scopes, and collections + * + * @param buckets - Either a string or array of bucket configurations + * @returns Normalized bucket resources for Couchbase API + */ +const normalizeBucketConfiguration = ( + buckets: + | string + | Array<{ + name: string; + scopes?: Array<{ + name: string; + collections?: string[]; + }>; + }> +) => { + if (typeof buckets === "string") { + // Simple string format - either "*" or comma-separated bucket names + const bucketNames = buckets + .split(",") + .map((bucket) => bucket.trim()) + .filter((bucket) => bucket.length > 0); + + // If "*" is present anywhere, grant access to all buckets, scopes, and collections + if (bucketNames.includes("*") || buckets === "*") { + return [{ name: "*" }]; + } + return bucketNames.map((bucketName) => ({ name: bucketName })); + } + + // Array of bucket objects with scopes and collections + // Check if any bucket is "*" - if so, grant access to all buckets, scopes, and collections + const hasWildcardBucket = buckets.some((bucket) => bucket.name === "*"); + + if (hasWildcardBucket) { + return [{ name: "*" }]; + } + + return buckets.map((bucket) => ({ + name: bucket.name, + scopes: bucket.scopes?.map((scope) => ({ + name: scope.name, + collections: scope.collections || [] + })) + })); +}; + const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => { const randomUsername = alphaNumericNanoId(12); if (!usernameTemplate) return sanitizeCouchbaseUsername(randomUsername); @@ -184,28 +235,7 @@ export const CouchbaseProvider = (): TDynamicProviderFns => { const createUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users`; - let bucketResources; - - if (typeof providerInputs.buckets === "string") { - // Simple string format - either "*" or comma-separated bucket names - const bucketNames = - providerInputs.buckets === "*" - ? ["*"] - : providerInputs.buckets - .split(",") - .map((bucket) => bucket.trim()) - .filter((bucket) => bucket.length > 0); - bucketResources = bucketNames.map((bucketName) => ({ name: bucketName })); - } else { - // Array of bucket objects with scopes and collections - bucketResources = providerInputs.buckets.map((bucket) => ({ - name: bucket.name, - scopes: bucket.scopes?.map((scope) => ({ - name: scope.name, - collections: scope.collections || [] - })) - })); - } + const bucketResources = normalizeBucketConfiguration(providerInputs.buckets); const userData: TCreateCouchbaseUser = { name: username, diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 43726b15a..ae1bcfc25 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -513,24 +513,50 @@ export const DynamicSecretCouchbaseSchema = z.object({ roles: z.array(z.string().trim().min(1)).min(1).describe("Roles to assign to the user"), buckets: z .union([ - z.string().trim().min(1).default("*"), - z.array( - z.object({ - name: z.string().trim().min(1).describe("Bucket name"), - scopes: z - .array( - z.object({ - name: z.string().trim().min(1).describe("Scope name"), - collections: z.array(z.string().trim().min(1)).optional().describe("Collection names") - }) - ) - .optional() - .describe("Scopes within the bucket") - }) - ) + z + .string() + .trim() + .min(1) + .default("*") + .refine((val) => { + if (val.includes(",")) { + const buckets = val + .split(",") + .map((b) => b.trim()) + .filter((b) => b.length > 0); + if (buckets.includes("*") && buckets.length > 1) { + return false; + } + } + return true; + }, "Cannot combine '*' with other bucket names"), + z + .array( + z.object({ + name: z.string().trim().min(1).describe("Bucket name"), + scopes: z + .array( + z.object({ + name: z.string().trim().min(1).describe("Scope name"), + collections: z.array(z.string().trim().min(1)).optional().describe("Collection names") + }) + ) + .optional() + .describe("Scopes within the bucket") + }) + ) + .refine((buckets) => { + const hasWildcard = buckets.some((bucket) => bucket.name === "*"); + if (hasWildcard && buckets.length > 1) { + return false; + } + return true; + }, "Cannot combine '*' bucket with other buckets") ]) .default("*") - .describe("Bucket configuration: '*' for all buckets or array of bucket objects with scopes and collections"), + .describe( + "Bucket configuration: '*' for all buckets, scopes, and collections or array of bucket objects with specific scopes and collections" + ), passwordRequirements: z .object({ length: z.number().min(8, "Password must be at least 8 characters").max(128), diff --git a/docs/documentation/platform/dynamic-secrets/couchbase.mdx b/docs/documentation/platform/dynamic-secrets/couchbase.mdx index 35e4cea57..6a803c1f8 100644 --- a/docs/documentation/platform/dynamic-secrets/couchbase.mdx +++ b/docs/documentation/platform/dynamic-secrets/couchbase.mdx @@ -54,8 +54,6 @@ Create an API Key in your Couchbase Cloud following the [official documentation] Database credential roles to assign to the generated user. Available options: - - **data_reader**: Read access to bucket data - - **data_writer**: Read and write access to bucket data - **read**: Read access to bucket data (alias for data_reader) - **write**: Read and write access to bucket data (alias for data_writer) @@ -244,8 +242,8 @@ To extend the life of the generated dynamic secret leases past its initial time The Couchbase dynamic secret integration supports the following database credential roles: -- **data_reader** / **read**: Provides read-only access to bucket data -- **data_writer** / **write**: Provides read and write access to bucket data +- **read**: Provides read-only access to bucket data +- **write**: Provides read and write access to bucket data These roles are specifically for database credentials and are different from Couchbase's administrative roles. They provide data-level access to buckets, scopes, and collections based on your configuration. @@ -257,5 +255,5 @@ These roles are specifically for database credentials and are different from Cou 1. **Invalid API Key**: Ensure your Couchbase Cloud API key has the necessary permissions to manage database users 2. **Invalid Organization/Project/Cluster IDs**: Verify that the provided IDs exist and are accessible with your API key -3. **Role Permission Errors**: Make sure you're using only the supported database credential roles (data_reader, data_writer, read, write) +3. **Role Permission Errors**: Make sure you're using only the supported database credential roles (read, write) 4. **Bucket Access Issues**: Ensure the specified buckets exist in your cluster and are accessible \ No newline at end of file diff --git a/frontend/src/hooks/api/dynamicSecret/mutation.ts b/frontend/src/hooks/api/dynamicSecret/mutation.ts index e347e4493..2d04334e6 100644 --- a/frontend/src/hooks/api/dynamicSecret/mutation.ts +++ b/frontend/src/hooks/api/dynamicSecret/mutation.ts @@ -43,12 +43,15 @@ export const useUpdateDynamicSecret = () => { ); return data.dynamicSecret; }, - onSuccess: (_, { path, environmentSlug, projectSlug }) => { + onSuccess: (_, { path, environmentSlug, projectSlug, name }) => { // TODO: optimize but currently don't pass projectId queryClient.invalidateQueries({ queryKey: dashboardKeys.all() }); queryClient.invalidateQueries({ queryKey: dynamicSecretKeys.list({ path, projectSlug, environmentSlug }) }); + queryClient.invalidateQueries({ + queryKey: dynamicSecretKeys.details({ path, projectSlug, environmentSlug, name }) + }); } }); }; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 09f6144d2..289bc1d04 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -363,13 +363,15 @@ export type TDynamicSecretProvider = projectId: string; clusterId: string; roles: string[]; - buckets: string | Array<{ - name: string; - scopes?: Array<{ - name: string; - collections?: string[]; - }>; - }>; + buckets: + | string + | Array<{ + name: string; + scopes?: Array<{ + name: string; + collections?: string[]; + }>; + }>; passwordRequirements?: { length: number; required: { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx index 67ac6ab3b..fd2eecaa6 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CouchbaseInputForm.tsx @@ -109,7 +109,11 @@ const BucketScopesConfiguration = ({ {scopeFields[scopeIndex]?.collections?.map( (collection: string, collectionIndex: number) => ( -
+
( ( field.onChange(e.target.value)} - placeholder="* (all buckets) or bucket1,bucket2,bucket3" + placeholder="* (all buckets, scopes & collections) or bucket1,bucket2,bucket3" /> )} @@ -625,7 +619,8 @@ export const CouchbaseInputForm = ({ Advanced Bucket Configuration
- Configure specific buckets with their scopes and collections + Configure specific buckets with their scopes and collections. Leave scopes + empty for access to all scopes in a bucket.