Address greptile suggestions

This commit is contained in:
Carlos Monastyrski
2025-08-13 11:23:55 -07:00
parent 7cf9d933da
commit b479406ba0
6 changed files with 589 additions and 527 deletions
@@ -4,6 +4,7 @@ import axios from "axios";
import RE2 from "re2";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
@@ -27,12 +28,17 @@ type TCreateCouchbaseUser = {
}[];
};
type CouchbaseUserResponse = {
id: string;
uuid?: string;
};
const sanitizeCouchbaseUsername = (username: string): string => {
// Couchbase username restrictions:
// - Cannot contain: ) ( > < , ; : " \ / ] [ ? = } {
// - Cannot begin with @ character
const forbiddenCharsPattern = new RE2("[\\)\\(><,;:\"\\\\\\[\\]\\?=\\}\\{]", "g");
const forbiddenCharsPattern = new RE2('[\\)\\(><,;:"\\\\\\[\\]\\?=\\}\\{]', "g");
let sanitized = forbiddenCharsPattern.replace(username, "-");
const leadingAtPattern = new RE2("^@+");
@@ -74,26 +80,26 @@ const generatePassword = (requirements?: PasswordRequirements): string => {
let remaining = length;
// Add required characters
for (let i = 0; i < required.lowercase; i++) {
for (let i = 0; i < required.lowercase; i += 1) {
password += lowercase[crypto.randomInt(lowercase.length)];
remaining--;
remaining -= 1;
}
for (let i = 0; i < required.uppercase; i++) {
for (let i = 0; i < required.uppercase; i += 1) {
password += uppercase[crypto.randomInt(uppercase.length)];
remaining--;
remaining -= 1;
}
for (let i = 0; i < required.digits; i++) {
for (let i = 0; i < required.digits; i += 1) {
password += digits[crypto.randomInt(digits.length)];
remaining--;
remaining -= 1;
}
for (let i = 0; i < required.symbols; i++) {
for (let i = 0; i < required.symbols; i += 1) {
password += symbols[crypto.randomInt(symbols.length)];
remaining--;
remaining -= 1;
}
// Fill remaining with random characters from all sets
const allChars = lowercase + uppercase + digits + symbols;
for (let i = 0; i < remaining; i++) {
for (let i = 0; i < remaining; i += 1) {
password += allChars[crypto.randomInt(allChars.length)];
}
@@ -109,41 +115,30 @@ const couchbaseApiRequest = async (
url: string,
apiKey: string,
data?: unknown
): Promise<unknown> => {
): Promise<CouchbaseUserResponse> => {
await blockLocalAndPrivateIpAddresses(url);
try {
const response = await axios({
method: method.toLowerCase() as any,
method: method.toLowerCase() as "get" | "post" | "put" | "delete",
url,
headers: {
"Authorization": `Bearer ${apiKey}`,
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
data: data || undefined,
timeout: 30000
});
return response.data;
} catch (error: any) {
if (error.response) {
// Server responded with error status
const errorData = error.response.data;
const errorMessage = typeof errorData === 'string' ? errorData : JSON.stringify(errorData);
throw new BadRequestError({
message: `Couchbase API error: ${error.response.status} ${error.response.statusText} - ${errorMessage}`
return response.data as CouchbaseUserResponse;
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [apiKey]
});
} else if (error.request) {
// Request made but no response received
throw new BadRequestError({
message: `Couchbase API request failed: ${error.message}`
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
} else {
// Error in request configuration
throw new BadRequestError({
message: `Couchbase API request error: ${error.message}`
});
}
}
};
@@ -193,15 +188,19 @@ export const CouchbaseProvider = (): TDynamicProviderFns => {
if (typeof providerInputs.buckets === "string") {
// Simple string format - either "*" or comma-separated bucket names
const bucketNames = providerInputs.buckets === "*"
const bucketNames =
providerInputs.buckets === "*"
? ["*"]
: providerInputs.buckets.split(",").map(bucket => bucket.trim()).filter(bucket => bucket.length > 0);
bucketResources = bucketNames.map(bucketName => ({ name: bucketName }));
: 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 => ({
bucketResources = providerInputs.buckets.map((bucket) => ({
name: bucket.name,
scopes: bucket.scopes?.map(scope => ({
scopes: bucket.scopes?.map((scope) => ({
name: scope.name,
collections: scope.collections || []
}))
@@ -211,15 +210,17 @@ export const CouchbaseProvider = (): TDynamicProviderFns => {
const userData: TCreateCouchbaseUser = {
name: username,
password,
access: [{
access: [
{
privileges: providerInputs.roles,
resources: {
buckets: bucketResources
}
}]
}
]
};
const response = await couchbaseApiRequest("POST", createUserUrl, providerInputs.auth.apiKey, userData) as any;
const response = await couchbaseApiRequest("POST", createUserUrl, providerInputs.auth.apiKey, userData);
const userUuid = response?.id || response?.uuid || username;
@@ -511,16 +511,26 @@ export const DynamicSecretCouchbaseSchema = z.object({
projectId: z.string().trim().min(1).describe("Project ID"),
clusterId: z.string().trim().min(1).describe("Cluster ID"),
roles: z.array(z.string().trim().min(1)).min(1).describe("Roles to assign to the user"),
buckets: z.union([
buckets: z
.union([
z.string().trim().min(1).default("*"),
z.array(z.object({
z.array(
z.object({
name: z.string().trim().min(1).describe("Bucket name"),
scopes: z.array(z.object({
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")
}))
]).default("*").describe("Bucket configuration: '*' for all buckets or array of bucket objects with scopes and collections"),
})
)
.optional()
.describe("Scopes within the bucket")
})
)
])
.default("*")
.describe("Bucket configuration: '*' for all buckets or array of bucket objects with scopes and collections"),
passwordRequirements: z
.object({
length: z.number().min(8, "Password must be at least 8 characters").max(128),
@@ -535,10 +545,13 @@ export const DynamicSecretCouchbaseSchema = z.object({
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 128;
}, "Sum of required characters cannot exceed 128"),
allowedSymbols: z.string().refine((symbols) => {
const forbiddenChars = ['<', '>', ';', '.', '*', '&', '|', '£'];
return !forbiddenChars.some(char => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £").optional()
allowedSymbols: z
.string()
.refine((symbols) => {
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "£"];
return !forbiddenChars.some((char) => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £")
.optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
@@ -110,7 +110,7 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
{{randomUsername}} // infisical-3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX
{{unixTimestamp}} // 17490641580
{{identity.name}} // testuser
{{random-5}} // x9k2m
{{random 5}} // x9k2m
{{truncate identity.name 4}} // test
{{replace identity.name 'user' 'replace'}} // testreplace
```
@@ -157,12 +157,12 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
![Provision Lease](../../../images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret.
@@ -170,7 +170,7 @@ Create an API Key in your Couchbase Cloud following the [official documentation]
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
![Provision Lease](../../../images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
@@ -229,12 +229,12 @@ The advanced bucket configuration allows you to specify granular access control:
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you to see the expiration time of the lease or delete a lease before its set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
![Provision Lease](../../../images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
![Provision Lease](../../../images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
@@ -1,7 +1,8 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -49,7 +50,6 @@ const BucketScopesConfiguration = ({
const bucket = Array.isArray(bucketsValue) ? bucketsValue[bucketIndex] : null;
const scopeFields = bucket?.scopes || [];
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
@@ -66,11 +66,12 @@ const BucketScopesConfiguration = ({
</div>
{scopeFields.map((_scope: any, scopeIndex: number) => (
<div key={scopeIndex} className="p-3 bg-mineshaft-700 rounded border border-mineshaft-600 space-y-3">
<div
key={`scope-${scopeIndex + 1}`}
className="space-y-3 rounded border border-mineshaft-600 bg-mineshaft-700 p-3"
>
<div className="flex items-center justify-between">
<h5 className="text-xs font-medium text-mineshaft-200">
Scope {scopeIndex + 1}
</h5>
<h5 className="text-xs font-medium text-mineshaft-200">Scope {scopeIndex + 1}</h5>
<IconButton
type="button"
variant="plain"
@@ -86,17 +87,13 @@ const BucketScopesConfiguration = ({
control={control}
name={`provider.buckets.${bucketIndex}.scopes.${scopeIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Scope Name"
isError={Boolean(error)}
errorText={error?.message}
>
<FormControl label="Scope Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="e.g., inventory, _default" className="text-sm" />
</FormControl>
)}
/>
<div className="pl-4 space-y-2">
<div className="space-y-2 pl-4">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-mineshaft-300">Collections</label>
<Button
@@ -110,15 +107,18 @@ const BucketScopesConfiguration = ({
</Button>
</div>
{scopeFields[scopeIndex]?.collections?.map((collection: string, collectionIndex: number) => (
<div key={`${bucketIndex}-${scopeIndex}-${collectionIndex}`} className="flex items-center space-x-2">
{scopeFields[scopeIndex]?.collections?.map(
(collection: string, collectionIndex: number) => (
<div key={collection} className="flex items-center space-x-2">
<FormControl className="flex-1">
<Input
value={collection || ""}
onChange={(e) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections[collectionIndex] = e.target.value;
currentBuckets[bucketIndex].scopes[scopeIndex].collections[
collectionIndex
] = e.target.value;
setValue("provider.buckets", currentBuckets);
}
}}
@@ -137,10 +137,12 @@ const BucketScopesConfiguration = ({
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
))}
)
)}
{(!scopeFields[scopeIndex]?.collections || scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs text-mineshaft-400 italic">
{(!scopeFields[scopeIndex]?.collections ||
scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs italic text-mineshaft-400">
No collections specified (access to all collections in scope)
</div>
)}
@@ -149,8 +151,8 @@ const BucketScopesConfiguration = ({
))}
{scopeFields.length === 0 && (
<div className="p-4 text-center border border-dashed border-mineshaft-600 rounded bg-mineshaft-700">
<p className="text-xs text-mineshaft-400 mb-2">
<div className="rounded border border-dashed border-mineshaft-600 bg-mineshaft-700 p-4 text-center">
<p className="mb-2 text-xs text-mineshaft-400">
No scopes configured (access to all scopes in bucket)
</p>
<Button
@@ -170,9 +172,21 @@ const BucketScopesConfiguration = ({
const couchbaseRoles = [
{ value: "data_reader", label: "Data Reader", description: "Read 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", label: "Write", description: "Read and write access to bucket data (alias for data_writer)" }
{
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",
label: "Write",
description: "Read and write access to bucket data (alias for data_writer)"
}
];
const passwordRequirementsSchema = z
@@ -189,10 +203,13 @@ const passwordRequirementsSchema = z
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 128;
}, "Sum of required characters cannot exceed 128"),
allowedSymbols: z.string().refine((symbols) => {
const forbiddenChars = ['<', '>', ';', '.', '*', '&', '|', '£'];
return !forbiddenChars.some(char => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £").optional()
allowedSymbols: z
.string()
.refine((symbols) => {
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "£"];
return !forbiddenChars.some((char) => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | £")
.optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
@@ -201,10 +218,14 @@ const passwordRequirementsSchema = z
const bucketSchema = z.object({
name: z.string().trim().min(1, "Bucket name is required"),
scopes: z.array(z.object({
scopes: z
.array(
z.object({
name: z.string().trim().min(1, "Scope name is required"),
collections: z.array(z.string().trim().min(1)).optional()
})).optional()
})
)
.optional()
});
const formSchema = z.object({
@@ -214,14 +235,11 @@ const formSchema = z.object({
projectId: z.string().trim().min(1),
clusterId: z.string().trim().min(1),
roles: z.array(z.string()).min(1, "At least one role must be selected"),
buckets: z.union([
z.string().trim().min(1),
z.array(bucketSchema)
]),
buckets: z.union([z.string().trim().min(1), z.array(bucketSchema)]),
useAdvancedBuckets: z.boolean().default(false),
passwordRequirements: passwordRequirementsSchema.optional(),
auth: z.object({
apiKey: z.string().trim()
apiKey: z.string().trim().min(1)
})
}),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -270,6 +288,7 @@ export const CouchbaseInputForm = ({
formState: { isSubmitting },
handleSubmit,
setValue,
getValues,
watch
} = useForm<TForm>({
resolver: zodResolver(formSchema),
@@ -329,7 +348,9 @@ export const CouchbaseInputForm = ({
const removeScope = (bucketIndex: number, scopeIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes) {
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter((_, i) => i !== scopeIndex);
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter(
(_, i) => i !== scopeIndex
);
setValue("provider.buckets", currentBuckets);
}
};
@@ -346,8 +367,9 @@ export const CouchbaseInputForm = ({
const removeCollection = (bucketIndex: number, scopeIndex: number, collectionIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections =
currentBuckets[bucketIndex].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
currentBuckets[bucketIndex].scopes[scopeIndex].collections = currentBuckets[
bucketIndex
].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
setValue("provider.buckets", currentBuckets);
}
};
@@ -462,10 +484,7 @@ export const CouchbaseInputForm = ({
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
placeholder="https://cloudapi.cloud.couchbase.com"
{...field}
/>
<Input placeholder="https://cloudapi.cloud.couchbase.com" {...field} />
</FormControl>
)}
/>
@@ -532,7 +551,7 @@ export const CouchbaseInputForm = ({
>
<FilterableSelect
isMulti
value={couchbaseRoles.filter(role => value?.includes(role.value))}
value={couchbaseRoles.filter((role) => value?.includes(role.value))}
onChange={(selectedRoles) => {
if (Array.isArray(selectedRoles)) {
onChange(selectedRoles.map((role: any) => role.value));
@@ -562,7 +581,7 @@ export const CouchbaseInputForm = ({
isChecked={value}
onCheckedChange={(checked) => {
onChange(checked);
const bucketsController = control._formValues.provider.buckets;
const bucketsController = getValues("provider.buckets");
if (checked && typeof bucketsController === "string") {
setValue("provider.buckets", []);
} else if (!checked && Array.isArray(bucketsController)) {
@@ -589,7 +608,7 @@ export const CouchbaseInputForm = ({
>
<Input
{...field}
value={typeof field.value === 'string' ? field.value : '*'}
value={typeof field.value === "string" ? field.value : "*"}
onChange={(e) => field.onChange(e.target.value)}
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
/>
@@ -621,8 +640,12 @@ export const CouchbaseInputForm = ({
</div>
<div className="space-y-4">
{Array.isArray(bucketsValue) && (bucketsValue as any[]).map((_, bucketIndex) => (
<div key={bucketIndex} className="p-4 border border-mineshaft-600 rounded bg-mineshaft-800 space-y-4">
{Array.isArray(bucketsValue) &&
(bucketsValue as any[]).map((_, bucketIndex) => (
<div
key={`bucket-${bucketIndex + 1}`}
className="space-y-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-mineshaft-200">
Bucket {bucketIndex + 1}
@@ -665,10 +688,8 @@ export const CouchbaseInputForm = ({
))}
{(!Array.isArray(bucketsValue) || bucketsValue.length === 0) && (
<div className="p-8 text-center border border-dashed border-mineshaft-600 rounded">
<p className="text-sm text-mineshaft-400 mb-2">
No buckets configured
</p>
<div className="rounded border border-dashed border-mineshaft-600 p-8 text-center">
<p className="mb-2 text-sm text-mineshaft-400">No buckets configured</p>
<Button
type="button"
variant="outline_bg"
@@ -731,7 +752,7 @@ export const CouchbaseInputForm = ({
<div className="flex items-center space-x-2">
<span>Password Configuration (optional)</span>
<Tooltip content="Couchbase password requirements: minimum 8 characters, at least 1 uppercase, 1 lowercase, 1 digit, 1 special character. Cannot contain: < > ; . * & | £">
<div className="h-4 w-4 rounded-full bg-mineshaft-600 text-xs text-mineshaft-300 flex items-center justify-center cursor-help">
<div className="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-mineshaft-600 text-xs text-mineshaft-300">
?
</div>
</Tooltip>
@@ -750,7 +771,7 @@ export const CouchbaseInputForm = ({
<Controller
control={control}
name="provider.passwordRequirements.length"
defaultValue={48}
defaultValue={12}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"
@@ -30,6 +30,7 @@ import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
import { AwsIamInputForm } from "./AwsIamInputForm";
import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
import { CassandraInputForm } from "./CassandraInputForm";
import { CouchbaseInputForm } from "./CouchbaseInputForm";
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
import { GcpIamInputForm } from "./GcpIamInputForm";
import { GithubInputForm } from "./GithubInputForm";
@@ -45,7 +46,6 @@ import { SnowflakeInputForm } from "./SnowflakeInputForm";
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
import { TotpInputForm } from "./TotpInputForm";
import { VerticaInputForm } from "./VerticaInputForm";
import { CouchbaseInputForm } from "./CouchbaseInputForm";
type Props = {
isOpen?: boolean;
@@ -1,7 +1,8 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -49,7 +50,6 @@ const BucketScopesConfiguration = ({
const bucket = Array.isArray(bucketsValue) ? bucketsValue[bucketIndex] : null;
const scopeFields = bucket?.scopes || [];
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
@@ -66,11 +66,12 @@ const BucketScopesConfiguration = ({
</div>
{scopeFields.map((_scope: any, scopeIndex: number) => (
<div key={scopeIndex} className="p-3 bg-mineshaft-700 rounded border border-mineshaft-600 space-y-3">
<div
key={`scope-${scopeIndex + 1}`}
className="space-y-3 rounded border border-mineshaft-600 bg-mineshaft-700 p-3"
>
<div className="flex items-center justify-between">
<h5 className="text-xs font-medium text-mineshaft-200">
Scope {scopeIndex + 1}
</h5>
<h5 className="text-xs font-medium text-mineshaft-200">Scope {scopeIndex + 1}</h5>
<IconButton
type="button"
variant="plain"
@@ -86,17 +87,13 @@ const BucketScopesConfiguration = ({
control={control}
name={`inputs.buckets.${bucketIndex}.scopes.${scopeIndex}.name`}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Scope Name"
isError={Boolean(error)}
errorText={error?.message}
>
<FormControl label="Scope Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="e.g., inventory, _default" className="text-sm" />
</FormControl>
)}
/>
<div className="pl-4 space-y-2">
<div className="space-y-2 pl-4">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-mineshaft-300">Collections</label>
<Button
@@ -110,15 +107,18 @@ const BucketScopesConfiguration = ({
</Button>
</div>
{scopeFields[scopeIndex]?.collections?.map((collection: string, collectionIndex: number) => (
<div key={`${bucketIndex}-${scopeIndex}-${collectionIndex}`} className="flex items-center space-x-2">
{scopeFields[scopeIndex]?.collections?.map(
(collection: string, collectionIndex: number) => (
<div key={collection} className="flex items-center space-x-2">
<FormControl className="flex-1">
<Input
value={collection || ""}
onChange={(e) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections[collectionIndex] = e.target.value;
currentBuckets[bucketIndex].scopes[scopeIndex].collections[
collectionIndex
] = e.target.value;
setValue("inputs.buckets", currentBuckets);
}
}}
@@ -136,10 +136,12 @@ const BucketScopesConfiguration = ({
<FontAwesomeIcon icon={faTrash} className="text-red-400" />
</IconButton>
</div>
))}
)
)}
{(!scopeFields[scopeIndex]?.collections || scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs text-mineshaft-400 italic">
{(!scopeFields[scopeIndex]?.collections ||
scopeFields[scopeIndex].collections.length === 0) && (
<div className="text-xs italic text-mineshaft-400">
No collections specified (access to all collections in scope)
</div>
)}
@@ -148,8 +150,8 @@ const BucketScopesConfiguration = ({
))}
{scopeFields.length === 0 && (
<div className="p-4 text-center border border-dashed border-mineshaft-600 rounded bg-mineshaft-700">
<p className="text-xs text-mineshaft-400 mb-2">
<div className="rounded border border-dashed border-mineshaft-600 bg-mineshaft-700 p-4 text-center">
<p className="mb-2 text-xs text-mineshaft-400">
No scopes configured (access to all scopes in bucket)
</p>
<Button
@@ -169,9 +171,21 @@ const BucketScopesConfiguration = ({
const couchbaseRoles = [
{ value: "data_reader", label: "Data Reader", description: "Read 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", label: "Write", description: "Read and write access to bucket data (alias for data_writer)" }
{
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",
label: "Write",
description: "Read and write access to bucket data (alias for data_writer)"
}
];
const passwordRequirementsSchema = z
@@ -188,10 +202,13 @@ const passwordRequirementsSchema = z
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 128;
}, "Sum of required characters cannot exceed 128"),
allowedSymbols: z.string().refine((symbols) => {
const forbiddenChars = ['<', '>', ';', '.', '*', '&', '|', '�'];
return !forbiddenChars.some(char => symbols?.includes(char));
}, "Cannot contain: < > ; . * & | �").optional()
allowedSymbols: z
.string()
.refine((symbols) => {
const forbiddenChars = ["<", ">", ";", ".", "*", "&", "|", "�"];
return !forbiddenChars.some((char) => symbols?.includes(char));
}, "Cannot contain: < > ; . * &")
.optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
@@ -200,10 +217,14 @@ const passwordRequirementsSchema = z
const bucketSchema = z.object({
name: z.string().trim().min(1, "Bucket name is required"),
scopes: z.array(z.object({
scopes: z
.array(
z.object({
name: z.string().trim().min(1, "Scope name is required"),
collections: z.array(z.string().trim().min(1)).optional()
})).optional()
})
)
.optional()
});
const formSchema = z.object({
@@ -214,10 +235,7 @@ const formSchema = z.object({
projectId: z.string().min(1),
clusterId: z.string().min(1),
roles: z.array(z.string()).min(1),
buckets: z.union([
z.string().trim().min(1),
z.array(bucketSchema)
]),
buckets: z.union([z.string().trim().min(1), z.array(bucketSchema)]),
useAdvancedBuckets: z.boolean().default(false),
passwordRequirements: passwordRequirementsSchema.optional(),
auth: z.object({
@@ -287,7 +305,7 @@ export const EditDynamicSecretCouchbaseForm = ({
metadata: dynamicSecret.metadata,
usernameTemplate: dynamicSecret.usernameTemplate,
inputs: {
...dynamicSecret.inputs as any,
...(dynamicSecret.inputs as any),
useAdvancedBuckets: Array.isArray((dynamicSecret.inputs as any)?.buckets)
}
}
@@ -324,7 +342,9 @@ export const EditDynamicSecretCouchbaseForm = ({
const removeScope = (bucketIndex: number, scopeIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes) {
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter((_, i) => i !== scopeIndex);
currentBuckets[bucketIndex].scopes = currentBuckets[bucketIndex].scopes.filter(
(_, i) => i !== scopeIndex
);
setValue("inputs.buckets", currentBuckets);
}
};
@@ -341,8 +361,9 @@ export const EditDynamicSecretCouchbaseForm = ({
const removeCollection = (bucketIndex: number, scopeIndex: number, collectionIndex: number) => {
const currentBuckets = Array.isArray(bucketsValue) ? [...bucketsValue] : [];
if (currentBuckets[bucketIndex]?.scopes?.[scopeIndex]?.collections) {
currentBuckets[bucketIndex].scopes[scopeIndex].collections =
currentBuckets[bucketIndex].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
currentBuckets[bucketIndex].scopes[scopeIndex].collections = currentBuckets[
bucketIndex
].scopes[scopeIndex].collections.filter((_, i) => i !== collectionIndex);
setValue("inputs.buckets", currentBuckets);
}
};
@@ -357,15 +378,19 @@ export const EditDynamicSecretCouchbaseForm = ({
}: TForm) => {
if (updateDynamicSecret.isPending) return;
const transformedInputs = inputs ? {
const transformedInputs = inputs
? {
...inputs,
buckets: inputs.useAdvancedBuckets ? inputs.buckets : (inputs.buckets as string)
} : inputs;
}
: inputs;
const finalInputs = transformedInputs ? (() => {
const finalInputs = transformedInputs
? (() => {
const { useAdvancedBuckets, ...rest } = transformedInputs;
return rest;
})() : transformedInputs;
})()
: transformedInputs;
try {
await updateDynamicSecret.mutateAsync({
@@ -378,7 +403,10 @@ export const EditDynamicSecretCouchbaseForm = ({
maxTTL: maxTTL || undefined,
newName: newName === dynamicSecret.name ? undefined : newName,
metadata,
usernameTemplate: !usernameTemplate || usernameTemplate === "{{randomUsername}}" ? undefined : usernameTemplate,
usernameTemplate:
!usernameTemplate || usernameTemplate === "{{randomUsername}}"
? undefined
: usernameTemplate,
inputs: finalInputs
}
});
@@ -392,7 +420,7 @@ export const EditDynamicSecretCouchbaseForm = ({
} catch (err) {
createNotification({
type: "error",
text: "Failed to update dynamic secret"
text: `Failed to update dynamic secret: ${err}`
});
}
};
@@ -468,10 +496,7 @@ export const EditDynamicSecretCouchbaseForm = ({
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
placeholder="https://cloudapi.cloud.couchbase.com"
{...field}
/>
<Input placeholder="https://cloudapi.cloud.couchbase.com" {...field} />
</FormControl>
)}
/>
@@ -534,7 +559,7 @@ export const EditDynamicSecretCouchbaseForm = ({
>
<FilterableSelect
isMulti
value={couchbaseRoles.filter(role => value?.includes(role.value))}
value={couchbaseRoles.filter((role) => value?.includes(role.value))}
onChange={(selectedRoles) => {
if (Array.isArray(selectedRoles)) {
onChange(selectedRoles.map((role: any) => role.value));
@@ -590,7 +615,7 @@ export const EditDynamicSecretCouchbaseForm = ({
>
<Input
{...field}
value={typeof field.value === 'string' ? field.value : '*'}
value={typeof field.value === "string" ? field.value : "*"}
onChange={(e) => field.onChange(e.target.value)}
placeholder="* (all buckets) or bucket1,bucket2,bucket3"
/>
@@ -622,8 +647,12 @@ export const EditDynamicSecretCouchbaseForm = ({
</div>
<div className="space-y-4">
{Array.isArray(bucketsValue) && (bucketsValue as any[]).map((_, bucketIndex) => (
<div key={bucketIndex} className="p-4 border border-mineshaft-600 rounded bg-mineshaft-800 space-y-4">
{Array.isArray(bucketsValue) &&
(bucketsValue as any[]).map((_, bucketIndex) => (
<div
key={`bucket-${bucketIndex + 1}`}
className="space-y-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-mineshaft-200">
Bucket {bucketIndex + 1}
@@ -666,10 +695,8 @@ export const EditDynamicSecretCouchbaseForm = ({
))}
{(!Array.isArray(bucketsValue) || bucketsValue.length === 0) && (
<div className="p-8 text-center border border-dashed border-mineshaft-600 rounded">
<p className="text-sm text-mineshaft-400 mb-2">
No buckets configured
</p>
<div className="rounded border border-dashed border-mineshaft-600 p-8 text-center">
<p className="mb-2 text-sm text-mineshaft-400">No buckets configured</p>
<Button
type="button"
variant="outline_bg"
@@ -729,7 +756,7 @@ export const EditDynamicSecretCouchbaseForm = ({
<div className="flex items-center space-x-2">
<span>Password Configuration (optional)</span>
<Tooltip content="Couchbase password requirements: minimum 8 characters, at least 1 uppercase, 1 lowercase, 1 digit, 1 special character. Cannot contain: < > ; . * & | �">
<div className="h-4 w-4 rounded-full bg-mineshaft-600 text-xs text-mineshaft-300 flex items-center justify-center cursor-help">
<div className="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-mineshaft-600 text-xs text-mineshaft-300">
?
</div>
</Tooltip>