mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-29 08:12:15 +00:00
feat: enhance vault provider management and environment variable resolution
- Introduced support for project and environment assignments for vault providers, allowing for more granular access control. - Updated the vault provider schema to include assignments and modified related components to handle these changes. - Enhanced UI to display assignment status and allow users to manage project and environment associations effectively. - Added validation to ensure assignments reference valid projects and environments within the organization. - Updated tests to cover new functionality related to vault provider assignments and environment variable resolution.
This commit is contained in:
@@ -62,6 +62,8 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
const completionSource = useEnvCompletionSource({
|
||||
projectEnv: data?.environment?.project?.env,
|
||||
environmentEnv: data?.environment?.env,
|
||||
projectId: data?.environment?.projectId,
|
||||
environmentId: data?.environment?.environmentId,
|
||||
});
|
||||
|
||||
const mutationMap = {
|
||||
|
||||
@@ -49,6 +49,8 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const completionSource = useEnvCompletionSource({
|
||||
projectEnv: data?.environment?.project?.env,
|
||||
environmentEnv: data?.environment?.env,
|
||||
projectId: data?.environment?.projectId,
|
||||
environmentId: data?.environment?.environmentId,
|
||||
});
|
||||
|
||||
const form = useForm<EnvironmentSchema>({
|
||||
|
||||
@@ -58,6 +58,8 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||
|
||||
const completionSource = useEnvCompletionSource({
|
||||
includeShared: false,
|
||||
projectId: data?.projectId,
|
||||
environmentId,
|
||||
});
|
||||
const form = useForm<UpdateEnvironment>({
|
||||
defaultValues: {
|
||||
|
||||
@@ -58,6 +58,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
||||
|
||||
const completionSource = useEnvCompletionSource({
|
||||
includeShared: false,
|
||||
projectId,
|
||||
});
|
||||
const form = useForm<UpdateProject>({
|
||||
defaultValues: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
import { vaultProviderIcons } from "@/components/icons/vault-provider-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -76,6 +77,12 @@ const VaultProviderSchema = z
|
||||
azureClientSecret: z.string(),
|
||||
dopplerProject: z.string(),
|
||||
dopplerConfig: z.string(),
|
||||
assignments: z.array(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
environmentIds: z.array(z.string()),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const isValidUrl = (value: string) => {
|
||||
@@ -215,6 +222,7 @@ const defaultValues: VaultProviderForm = {
|
||||
azureClientSecret: "",
|
||||
dopplerProject: "",
|
||||
dopplerConfig: "",
|
||||
assignments: [],
|
||||
};
|
||||
|
||||
const buildConfig = (data: VaultProviderForm) => {
|
||||
@@ -303,6 +311,39 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
|
||||
});
|
||||
|
||||
const providerType = form.watch("providerType");
|
||||
const assignments = form.watch("assignments");
|
||||
const { data: orgProjects } = api.project.all.useQuery();
|
||||
|
||||
const setAssignments = (
|
||||
next: { projectId: string; environmentIds: string[] }[],
|
||||
) => form.setValue("assignments", next, { shouldValidate: true });
|
||||
|
||||
const toggleProject = (targetProjectId: string) => {
|
||||
const exists = assignments.some((a) => a.projectId === targetProjectId);
|
||||
setAssignments(
|
||||
exists
|
||||
? assignments.filter((a) => a.projectId !== targetProjectId)
|
||||
: [...assignments, { projectId: targetProjectId, environmentIds: [] }],
|
||||
);
|
||||
};
|
||||
|
||||
const toggleEnvironment = (
|
||||
targetProjectId: string,
|
||||
environmentId: string,
|
||||
) => {
|
||||
setAssignments(
|
||||
assignments.map((a) => {
|
||||
if (a.projectId !== targetProjectId) return a;
|
||||
const has = a.environmentIds.includes(environmentId);
|
||||
return {
|
||||
...a,
|
||||
environmentIds: has
|
||||
? a.environmentIds.filter((e) => e !== environmentId)
|
||||
: [...a.environmentIds, environmentId],
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (provider) {
|
||||
@@ -310,6 +351,7 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
|
||||
...defaultValues,
|
||||
name: provider.name,
|
||||
providerType: provider.config.providerType,
|
||||
assignments: provider.assignments ?? [],
|
||||
...(provider.config.providerType === "hashicorp" && {
|
||||
url: provider.config.url,
|
||||
token: provider.config.token,
|
||||
@@ -351,6 +393,7 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
|
||||
const payload: any = {
|
||||
name: data.name,
|
||||
config: buildConfig(data),
|
||||
assignments: data.assignments,
|
||||
...(vaultProviderId && { vaultProviderId }),
|
||||
};
|
||||
await mutateAsync(payload)
|
||||
@@ -751,8 +794,8 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<FormDescription>
|
||||
App Registration with the "Key Vault Secrets User" role on
|
||||
the vault. Reference format:{" "}
|
||||
App Registration with the "Key Vault Secrets User" role on the
|
||||
vault. Reference format:{" "}
|
||||
<code>{"${{vault.<name>.secret-name}}"}</code>
|
||||
</FormDescription>
|
||||
</>
|
||||
@@ -816,6 +859,71 @@ export const HandleVaultProvider = ({ vaultProviderId }: Props) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-lg border p-3">
|
||||
<FormLabel>Access</FormLabel>
|
||||
<FormDescription>
|
||||
This provider can only be referenced from the selected projects.
|
||||
Pick environments to narrow it further — none selected means all
|
||||
environments of that project.
|
||||
</FormDescription>
|
||||
<div className="flex flex-col gap-2">
|
||||
{orgProjects?.map((project) => {
|
||||
const assignment = assignments.find(
|
||||
(a) => a.projectId === project.projectId,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={project.projectId}
|
||||
className="flex flex-col gap-1.5"
|
||||
>
|
||||
<label className="flex flex-row items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={!!assignment}
|
||||
onCheckedChange={() =>
|
||||
toggleProject(project.projectId)
|
||||
}
|
||||
/>
|
||||
{project.name}
|
||||
</label>
|
||||
{assignment && (
|
||||
<div className="flex flex-row flex-wrap gap-3 pl-6">
|
||||
{project.environments?.map((environment) => (
|
||||
<label
|
||||
key={environment.environmentId}
|
||||
className="flex flex-row items-center gap-1.5 text-xs text-muted-foreground cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={assignment.environmentIds.includes(
|
||||
environment.environmentId,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
toggleEnvironment(
|
||||
project.projectId,
|
||||
environment.environmentId,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{environment.name}
|
||||
</label>
|
||||
))}
|
||||
{assignment.environmentIds.length === 0 && (
|
||||
<span className="text-xs text-muted-foreground italic self-center">
|
||||
All environments
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{form.formState.errors.assignments && (
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{form.formState.errors.assignments.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex w-full flex-row justify-between gap-2 sm:justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -80,6 +80,18 @@ export const ShowVaultProviders = () => {
|
||||
{provider.name}
|
||||
</span>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{provider.assignments.length === 0 ? (
|
||||
<Badge variant="destructive">
|
||||
Not assigned
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
{provider.assignments.length}{" "}
|
||||
{provider.assignments.length === 1
|
||||
? "project"
|
||||
: "projects"}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline">
|
||||
{providerLabels[provider.providerType] ??
|
||||
provider.providerType}
|
||||
|
||||
@@ -163,6 +163,11 @@ const RESOURCE_META: Record<string, { label: string; description: string }> = {
|
||||
label: "Audit Logs",
|
||||
description: "View the audit log of actions performed in the organization",
|
||||
},
|
||||
vaultProvider: {
|
||||
label: "Secrets Providers",
|
||||
description:
|
||||
"Manage external secret managers (HashiCorp Vault, AWS, Azure, Infisical, Doppler) and where their secrets can be referenced",
|
||||
},
|
||||
};
|
||||
|
||||
/** Descriptions for each action within a resource */
|
||||
@@ -419,6 +424,22 @@ const ACTION_META: Record<
|
||||
auditLog: {
|
||||
read: { label: "Read", description: "View the audit log history" },
|
||||
},
|
||||
vaultProvider: {
|
||||
read: {
|
||||
label: "Read",
|
||||
description: "View providers and secret names for env autocomplete",
|
||||
},
|
||||
create: {
|
||||
label: "Create",
|
||||
description: "Connect new secret providers and test their connection",
|
||||
},
|
||||
update: {
|
||||
label: "Update",
|
||||
description:
|
||||
"Edit provider credentials and project/environment assignments",
|
||||
},
|
||||
delete: { label: "Delete", description: "Remove secret providers" },
|
||||
},
|
||||
};
|
||||
|
||||
/** Resources that should be hidden from the custom role editor (better-auth internals) */
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Options {
|
||||
projectEnv?: string | null;
|
||||
environmentEnv?: string | null;
|
||||
includeShared?: boolean;
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
}
|
||||
|
||||
const parseKeys = (env?: string | null) =>
|
||||
@@ -55,9 +57,22 @@ export const useEnvCompletionSource = ({
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
includeShared = true,
|
||||
projectId,
|
||||
environmentId,
|
||||
}: Options = {}) => {
|
||||
const { data: providers } = api.vaultProvider.all.useQuery();
|
||||
const { data: allProviders } = api.vaultProvider.all.useQuery();
|
||||
const utils = api.useUtils();
|
||||
const providers = projectId
|
||||
? allProviders?.filter((provider) =>
|
||||
provider.assignments?.some(
|
||||
(assignment) =>
|
||||
assignment.projectId === projectId &&
|
||||
(assignment.environmentIds.length === 0 ||
|
||||
!environmentId ||
|
||||
assignment.environmentIds.includes(environmentId)),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
return useCallback(
|
||||
async (context: CompletionContext): Promise<CompletionResult | null> => {
|
||||
|
||||
Reference in New Issue
Block a user