mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 23:27:35 +00:00
Address PR comments
This commit is contained in:
@@ -51,6 +51,57 @@ const sanitizeCouchbaseUsername = (username: string): string => {
|
|||||||
return sanitized;
|
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 generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||||
const randomUsername = alphaNumericNanoId(12);
|
const randomUsername = alphaNumericNanoId(12);
|
||||||
if (!usernameTemplate) return sanitizeCouchbaseUsername(randomUsername);
|
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`;
|
const createUserUrl = `${providerInputs.url}/v4/organizations/${providerInputs.orgId}/projects/${providerInputs.projectId}/clusters/${providerInputs.clusterId}/users`;
|
||||||
|
|
||||||
let bucketResources;
|
const bucketResources = normalizeBucketConfiguration(providerInputs.buckets);
|
||||||
|
|
||||||
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 userData: TCreateCouchbaseUser = {
|
const userData: TCreateCouchbaseUser = {
|
||||||
name: username,
|
name: username,
|
||||||
|
|||||||
@@ -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"),
|
roles: z.array(z.string().trim().min(1)).min(1).describe("Roles to assign to the user"),
|
||||||
buckets: z
|
buckets: z
|
||||||
.union([
|
.union([
|
||||||
z.string().trim().min(1).default("*"),
|
z
|
||||||
z.array(
|
.string()
|
||||||
z.object({
|
.trim()
|
||||||
name: z.string().trim().min(1).describe("Bucket name"),
|
.min(1)
|
||||||
scopes: z
|
.default("*")
|
||||||
.array(
|
.refine((val) => {
|
||||||
z.object({
|
if (val.includes(",")) {
|
||||||
name: z.string().trim().min(1).describe("Scope name"),
|
const buckets = val
|
||||||
collections: z.array(z.string().trim().min(1)).optional().describe("Collection names")
|
.split(",")
|
||||||
})
|
.map((b) => b.trim())
|
||||||
)
|
.filter((b) => b.length > 0);
|
||||||
.optional()
|
if (buckets.includes("*") && buckets.length > 1) {
|
||||||
.describe("Scopes within the bucket")
|
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("*")
|
.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
|
passwordRequirements: z
|
||||||
.object({
|
.object({
|
||||||
length: z.number().min(8, "Password must be at least 8 characters").max(128),
|
length: z.number().min(8, "Password must be at least 8 characters").max(128),
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
|
|||||||
|
|
||||||
<ParamField path="Roles" type="array" required>
|
<ParamField path="Roles" type="array" required>
|
||||||
Database credential roles to assign to the generated user. Available options:
|
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)
|
- **read**: Read access to bucket data (alias for data_reader)
|
||||||
- **write**: Read and write access to bucket data (alias for data_writer)
|
- **write**: Read and write access to bucket data (alias for data_writer)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -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:
|
The Couchbase dynamic secret integration supports the following database credential roles:
|
||||||
|
|
||||||
- **data_reader** / **read**: Provides read-only access to bucket data
|
- **read**: Provides read-only access to bucket data
|
||||||
- **data_writer** / **write**: Provides read and write access to bucket data
|
- **write**: Provides read and write access to bucket data
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
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.
|
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
|
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
|
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
|
4. **Bucket Access Issues**: Ensure the specified buckets exist in your cluster and are accessible
|
||||||
@@ -43,12 +43,15 @@ export const useUpdateDynamicSecret = () => {
|
|||||||
);
|
);
|
||||||
return data.dynamicSecret;
|
return data.dynamicSecret;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { path, environmentSlug, projectSlug }) => {
|
onSuccess: (_, { path, environmentSlug, projectSlug, name }) => {
|
||||||
// TODO: optimize but currently don't pass projectId
|
// TODO: optimize but currently don't pass projectId
|
||||||
queryClient.invalidateQueries({ queryKey: dashboardKeys.all() });
|
queryClient.invalidateQueries({ queryKey: dashboardKeys.all() });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: dynamicSecretKeys.list({ path, projectSlug, environmentSlug })
|
queryKey: dynamicSecretKeys.list({ path, projectSlug, environmentSlug })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: dynamicSecretKeys.details({ path, projectSlug, environmentSlug, name })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -363,13 +363,15 @@ export type TDynamicSecretProvider =
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
clusterId: string;
|
clusterId: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
buckets: string | Array<{
|
buckets:
|
||||||
name: string;
|
| string
|
||||||
scopes?: Array<{
|
| Array<{
|
||||||
name: string;
|
name: string;
|
||||||
collections?: string[];
|
scopes?: Array<{
|
||||||
}>;
|
name: string;
|
||||||
}>;
|
collections?: string[];
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
passwordRequirements?: {
|
passwordRequirements?: {
|
||||||
length: number;
|
length: number;
|
||||||
required: {
|
required: {
|
||||||
|
|||||||
+14
-19
@@ -109,7 +109,11 @@ const BucketScopesConfiguration = ({
|
|||||||
|
|
||||||
{scopeFields[scopeIndex]?.collections?.map(
|
{scopeFields[scopeIndex]?.collections?.map(
|
||||||
(collection: string, collectionIndex: number) => (
|
(collection: string, collectionIndex: number) => (
|
||||||
<div key={collection} className="flex items-center space-x-2">
|
<div
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={`collection-${bucketIndex}-${scopeIndex}-${collectionIndex}`}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
<FormControl className="flex-1">
|
<FormControl className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
value={collection || ""}
|
value={collection || ""}
|
||||||
@@ -171,21 +175,11 @@ const BucketScopesConfiguration = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const couchbaseRoles = [
|
const couchbaseRoles = [
|
||||||
{ value: "data_reader", label: "Data Reader", description: "Read access to bucket data" },
|
{ value: "read", label: "Read", description: "Read-only access to bucket data" },
|
||||||
{
|
|
||||||
value: "data_writer",
|
|
||||||
label: "Data Writer",
|
|
||||||
description: "Read and write access to bucket data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "read",
|
|
||||||
label: "Read",
|
|
||||||
description: "Read access to bucket data (alias for data_reader)"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "write",
|
value: "write",
|
||||||
label: "Write",
|
label: "Write",
|
||||||
description: "Read and write access to bucket data (alias for data_writer)"
|
description: "Full write access to bucket data"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -295,7 +289,7 @@ export const CouchbaseInputForm = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
provider: {
|
provider: {
|
||||||
url: "https://cloudapi.cloud.couchbase.com",
|
url: "https://cloudapi.cloud.couchbase.com",
|
||||||
roles: ["data_reader"],
|
roles: ["read"],
|
||||||
buckets: "*",
|
buckets: "*",
|
||||||
useAdvancedBuckets: false,
|
useAdvancedBuckets: false,
|
||||||
passwordRequirements: {
|
passwordRequirements: {
|
||||||
@@ -540,7 +534,7 @@ export const CouchbaseInputForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="provider.roles"
|
name="provider.roles"
|
||||||
defaultValue={["data_reader"]}
|
defaultValue={["read"]}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Roles"
|
label="Roles"
|
||||||
@@ -574,7 +568,7 @@ export const CouchbaseInputForm = ({
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Advanced Bucket Configuration"
|
label="Advanced Bucket Configuration"
|
||||||
helperText="Enable to configure specific scopes and collections within buckets"
|
helperText="Enable to configure specific buckets, scopes and collections. When disabled, '*' grants access to all buckets, scopes, and collections."
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
id="advanced-buckets-switch"
|
id="advanced-buckets-switch"
|
||||||
@@ -604,13 +598,13 @@ export const CouchbaseInputForm = ({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets"
|
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets, scopes, and collections"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
value={typeof field.value === "string" ? field.value : "*"}
|
value={typeof field.value === "string" ? field.value : "*"}
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
|
placeholder="* (all buckets, scopes & collections) or bucket1,bucket2,bucket3"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -625,7 +619,8 @@ export const CouchbaseInputForm = ({
|
|||||||
Advanced Bucket Configuration
|
Advanced Bucket Configuration
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-mineshaft-400">
|
<div className="text-sm text-mineshaft-400">
|
||||||
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.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
+14
-19
@@ -109,7 +109,11 @@ const BucketScopesConfiguration = ({
|
|||||||
|
|
||||||
{scopeFields[scopeIndex]?.collections?.map(
|
{scopeFields[scopeIndex]?.collections?.map(
|
||||||
(collection: string, collectionIndex: number) => (
|
(collection: string, collectionIndex: number) => (
|
||||||
<div key={collection} className="flex items-center space-x-2">
|
<div
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={`collection-${bucketIndex}-${scopeIndex}-${collectionIndex}`}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
<FormControl className="flex-1">
|
<FormControl className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
value={collection || ""}
|
value={collection || ""}
|
||||||
@@ -133,7 +137,7 @@ const BucketScopesConfiguration = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeCollection(bucketIndex, scopeIndex, collectionIndex)}
|
onClick={() => removeCollection(bucketIndex, scopeIndex, collectionIndex)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
|
<FontAwesomeIcon icon={faTrash} className="mb-4 text-red-400" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -170,21 +174,11 @@ const BucketScopesConfiguration = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const couchbaseRoles = [
|
const couchbaseRoles = [
|
||||||
{ value: "data_reader", label: "Data Reader", description: "Read access to bucket data" },
|
{ value: "read", label: "Read", description: "Read-only access to bucket data" },
|
||||||
{
|
|
||||||
value: "data_writer",
|
|
||||||
label: "Data Writer",
|
|
||||||
description: "Read and write access to bucket data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "read",
|
|
||||||
label: "Read",
|
|
||||||
description: "Read access to bucket data (alias for data_reader)"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "write",
|
value: "write",
|
||||||
label: "Write",
|
label: "Write",
|
||||||
description: "Read and write access to bucket data (alias for data_writer)"
|
description: "Full write access to bucket data"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -548,7 +542,7 @@ export const EditDynamicSecretCouchbaseForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="inputs.roles"
|
name="inputs.roles"
|
||||||
defaultValue={["data_reader"]}
|
defaultValue={["read"]}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Roles"
|
label="Roles"
|
||||||
@@ -581,7 +575,7 @@ export const EditDynamicSecretCouchbaseForm = ({
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Advanced Bucket Configuration"
|
label="Advanced Bucket Configuration"
|
||||||
helperText="Enable to configure specific scopes and collections within buckets"
|
helperText="Enable to configure specific buckets, scopes and collections. When disabled, '*' grants access to all buckets, scopes, and collections."
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
id="advanced-buckets-switch"
|
id="advanced-buckets-switch"
|
||||||
@@ -611,13 +605,13 @@ export const EditDynamicSecretCouchbaseForm = ({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets"
|
helperText="Specify bucket names separated by commas (e.g., 'bucket1,bucket2') or use '*' for all buckets, scopes, and collections"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
value={typeof field.value === "string" ? field.value : "*"}
|
value={typeof field.value === "string" ? field.value : "*"}
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
|
placeholder="* (all buckets, scopes & collections) or bucket1,bucket2,bucket3"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -632,7 +626,8 @@ export const EditDynamicSecretCouchbaseForm = ({
|
|||||||
Advanced Bucket Configuration
|
Advanced Bucket Configuration
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-mineshaft-400">
|
<div className="text-sm text-mineshaft-400">
|
||||||
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.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user