fix(view-secret-value): backwards compatibility for read

This commit is contained in:
Daniel Hougaard
2025-03-08 03:40:51 +04:00
parent d74197aeb4
commit f1ba64aa66
24 changed files with 708 additions and 882 deletions

View File

@@ -8,7 +8,8 @@ export enum ProjectPermissionActions {
}
export enum ProjectPermissionSecretActions {
DescribeSecret = "read",
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",

View File

@@ -0,0 +1,36 @@
import { MongoAbility, subject } from "@casl/ability";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import {
ProjectPermissionSecretActions,
ProjectPermissionSub,
SecretSubjectFields
} from "@app/context/ProjectPermissionContext/types";
export function secretsPermissionCan(
permission: MongoAbility<ProjectPermissionSet>,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
>,
subjectFields?: SecretSubjectFields
) {
let canNewPermission = false;
let canOldPermission = false;
if (subjectFields) {
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
return canNewPermission || canOldPermission;
}

View File

@@ -303,13 +303,9 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
label: "Describe Secret (default)",
label: "Read (default)",
value: "read"
},
{
label: "Read Value (optional)",
value: "readValue"
},
{
label: "Write (optional)",
value: "write"

View File

@@ -35,12 +35,13 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
title,
isDisabled
}: Props<T>) => {
const { control } = useFormContext<TFormSchema>();
const { control, watch } = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${subject}`
});
const [isOpen, setIsOpen] = useToggle();
// const [hideFullReadAccess, setHideFullReadAccess] = useState(false);
if (!items.fields.length) return <div />;
@@ -71,119 +72,138 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
</div>
{isOpen && (
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
{items.fields.map((el, rootIndex) => (
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md">
{isConditionalSubjects(subject) && (
<div className="mb-6 mt-4 flex w-full items-center text-gray-300">
<div className="w-1/4">Permission</div>
<div className="mr-4 w-1/4">
<Controller
defaultValue={false as any}
name={`permissions.${subject}.${rootIndex}.inverted`}
render={({ field }) => (
<Select
value={String(field.value)}
onValueChange={(val) => field.onChange(val === "true")}
containerClassName="w-full"
className="w-full"
isDisabled={isDisabled}
>
<SelectItem value="false">Allow</SelectItem>
<SelectItem value="true">Forbid</SelectItem>
</Select>
)}
/>
</div>
<div>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
</Tooltip>
</div>
</div>
)}
<div className="flex text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }) => {
if (typeof value !== "string") return undefined;
{items.fields.map((el, rootIndex) => {
let isFullReadAccessEnabled = false;
return (
if (subject === ProjectPermissionSub.Secrets) {
isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
}
return (
<div
key={el.id}
className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md"
>
{isConditionalSubjects(subject) && (
<div className="mb-6 mt-4 flex w-full items-center text-gray-300">
<div className="w-1/4">Permission</div>
<div className="mr-4 w-1/4">
<Controller
key={`${el.id}-${label}`}
name={`permissions.${subject}.${rootIndex}.${value}` as any}
control={control}
defaultValue={false}
render={({ field }) => {
return (
<div className="flex items-center justify-center">
<Checkbox
isDisabled={isDisabled}
isChecked={Boolean(field.value)}
onCheckedChange={field.onChange}
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
>
{label}
</Checkbox>
</div>
);
}}
defaultValue={false as any}
name={`permissions.${subject}.${rootIndex}.inverted`}
render={({ field }) => (
<Select
value={String(field.value)}
onValueChange={(val) => field.onChange(val === "true")}
containerClassName="w-full"
className="w-full"
isDisabled={isDisabled}
>
<SelectItem value="false">Allow</SelectItem>
<SelectItem value="true">Forbid</SelectItem>
</Select>
)}
/>
);
</div>
<div>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
</Tooltip>
</div>
</div>
)}
<div className="flex text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }, index) => {
if (typeof value !== "string") return undefined;
if (
subject === ProjectPermissionSub.Secrets &&
value === "read" &&
!isFullReadAccessEnabled
) {
return null;
}
return (
<Controller
key={`${el.id}-${index + 1}`}
name={`permissions.${subject}.${rootIndex}.${value}` as any}
control={control}
defaultValue={false}
render={({ field }) => {
return (
<div className="flex items-center justify-center">
<Checkbox
isDisabled={isDisabled}
isChecked={Boolean(field.value)}
onCheckedChange={field.onChange}
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
>
{label}
</Checkbox>
</div>
);
}}
/>
);
})}
</div>
</div>
{children &&
cloneElement(children, {
position: rootIndex
})}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
items.insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => items.remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div>
</div>
{children &&
cloneElement(children, {
position: rootIndex
})}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
items.insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => items.remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div>
</div>
))}
);
})}
</div>
)}
</div>

View File

@@ -1,5 +1,9 @@
import { ReactNode } from "react";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
@@ -24,11 +28,12 @@ const GeneralPolicyActionSchema = z.object({
});
const SecretPolicyActionSchema = z.object({
read: z.boolean().optional(), // describe secret
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional(),
readValue: z.boolean().optional()
[ProjectPermissionSecretActions.DescribeAndReadValue]: z.boolean().optional(), // existing read, gives both describe and read value
[ProjectPermissionSecretActions.DescribeSecret]: z.boolean().optional(), // describe secret, cannot read value
[ProjectPermissionSecretActions.ReadValue]: z.boolean().optional(), // read value
[ProjectPermissionSecretActions.Edit]: z.boolean().optional(), // edit secret
[ProjectPermissionSecretActions.Delete]: z.boolean().optional(), // delete secret
[ProjectPermissionSecretActions.Create]: z.boolean().optional() // create secret
});
const CmekPolicyActionSchema = z.object({
@@ -294,19 +299,24 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
}
if (subject === ProjectPermissionSub.Secrets) {
const canRead = action.includes(ProjectPermissionSecretActions.DescribeSecret);
const canDescribeAndReadValue = action.includes(
ProjectPermissionSecretActions.DescribeAndReadValue
);
const canDescribe = action.includes(ProjectPermissionSecretActions.DescribeSecret);
const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
const canEdit = action.includes(ProjectPermissionSecretActions.Edit);
const canDelete = action.includes(ProjectPermissionSecretActions.Delete);
const canCreate = action.includes(ProjectPermissionSecretActions.Create);
const canReadValue = action.includes(ProjectPermissionSecretActions.ReadValue);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
read: canRead,
describeSecret: canDescribe,
read: canDescribeAndReadValue,
readValue: canReadValue,
create: canCreate,
edit: canEdit,
delete: canDelete,
readValue: canReadValue,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
@@ -501,7 +511,7 @@ export type TProjectPermissionObject = {
[K in ProjectPermissionSub]: {
title: string;
actions: {
label: string;
label: string | ReactNode;
value: keyof Omit<
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
"conditions" | "inverted"
@@ -514,11 +524,35 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: {
title: "Secrets",
actions: [
{ label: "Describe Secret", value: "read" },
{ label: "Create", value: "create" },
{ label: "Read Value", value: "readValue" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
{
label: (
<div className="flex items-center gap-1.5">
<p className="opacity-60">
Read <span className="text-xs opacity-80">(legacy)</span>
</p>
<Tooltip
className="overflow-hidden whitespace-normal"
content={
<div>
This is a legacy action and will be removed in the future.
<br />
<br /> You should instead use the{" "}
<strong className="font-semibold">Describe Secret</strong> and{" "}
<strong className="font-semibold">Read Value</strong> actions.
</div>
}
>
<FontAwesomeIcon icon={faWarning} className="mt-1 text-yellow-500" size="sm" />
</Tooltip>
</div>
),
value: ProjectPermissionSecretActions.DescribeAndReadValue
},
{ label: "Describe Secret", value: ProjectPermissionSecretActions.DescribeSecret },
{ label: "Read Value", value: ProjectPermissionSecretActions.ReadValue },
{ label: "Modify", value: ProjectPermissionSecretActions.Edit },
{ label: "Remove", value: ProjectPermissionSecretActions.Delete },
{ label: "Create", value: ProjectPermissionSecretActions.Create }
]
},
[ProjectPermissionSub.SecretFolders]: {

View File

@@ -15,6 +15,7 @@ import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionCo
import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
import { secretsPermissionCan } from "@app/lib/fn/permission";
enum SecretActionType {
Created = "created",
@@ -51,8 +52,11 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
});
const isSecretInEnvReadOnly =
permission.can(ProjectPermissionSecretActions.DescribeSecret, secretPermissionSubject) &&
permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
secretsPermissionCan(
permission,
ProjectPermissionSecretActions.DescribeSecret,
secretPermissionSubject
) && permission.cannot(ProjectPermissionSecretActions.Edit, secretPermissionSubject);
if (isSecretInEnvReadOnly) {
return true;
}

View File

@@ -38,6 +38,7 @@ import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { secretsPermissionCan } from "@app/lib/fn/permission";
import { SecretTableResourceCount } from "../OverviewPage/components/SecretTableResourceCount";
import { SecretV2MigrationSection } from "../OverviewPage/components/SecretV2MigrationSection";
@@ -103,23 +104,27 @@ const Page = () => {
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const secretPath = (routerQueryParams.secretPath as string) || "/";
const canReadSecret = permission.can(
const canReadSecret = secretsPermissionCan(
permission,
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
{
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
}
);
const canReadSecretValue = permission.can(
const canReadSecretValue = secretsPermissionCan(
permission,
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
{
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
}
);
const canReadSecretImports = permission.can(

View File

@@ -57,6 +57,7 @@ import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { secretsPermissionCan } from "@app/lib/fn/permission";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -140,26 +141,24 @@ export const SecretDetailSidebar = ({
})
);
const cannotReadSecretValue = permission.cannot(
const cannotReadSecretValue = !secretsPermissionCan(
permission,
ProjectPermissionSecretActions.ReadValue,
subject(ProjectPermissionSub.Secrets, {
{
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
}
);
const isReadOnly =
permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
) &&
secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
}) &&
cannotEditSecret &&
cannotReadSecretValue;

View File

@@ -47,6 +47,7 @@ import {
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import { secretsPermissionCan } from "@app/lib/fn/permission";
import {
FontAwesomeSpriteName,
formSchema,
@@ -131,15 +132,12 @@ export const SecretItem = memo(
});
const isReadOnly =
permission.can(
ProjectPermissionSecretActions.DescribeSecret,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})
) &&
secretsPermissionCan(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
}) &&
permission.cannot(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {