feat: updated frontend project permission logic

This commit is contained in:
=
2024-10-18 00:33:35 +05:30
parent 181821f8f5
commit b6dc6ffc01
29 changed files with 663 additions and 274 deletions
@@ -1,23 +1,25 @@
import { FunctionComponent, ReactNode } from "react"; import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react"; import { AbilityTuple, MongoAbility } from "@casl/ability";
import { Can } from "@casl/react";
import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext";
import { Tooltip } from "../v2"; import { Tooltip } from "../v2/Tooltip";
type Props = { type Props<T extends AbilityTuple> = {
label?: ReactNode; label?: ReactNode;
// this prop is used when there exist already a tooltip as helper text for users // this prop is used when there exist already a tooltip as helper text for users
// so when permission is allowed same tooltip will be reused to show helpertext // so when permission is allowed same tooltip will be reused to show helpertext
renderTooltip?: boolean; renderTooltip?: boolean;
allowedLabel?: string; allowedLabel?: string;
// BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
// For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error passThrough?: boolean;
// tried a lot combinations I: T[0];
// REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15 a: T[1];
} & BoundCanProps<any>; ability?: MongoAbility<T>;
};
export const ProjectPermissionCan: FunctionComponent<Props> = ({ export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>> = ({
label = "Access restricted", label = "Access restricted",
children, children,
passThrough = true, passThrough = true,
@@ -31,9 +33,7 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
{(isAllowed, ability) => { {(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type. // akhilmhdh: This is set as type due to error in casl react type.
const finalChild = const finalChild =
typeof children === "function" typeof children === "function" ? children(isAllowed, ability as any) : children;
? children(isAllowed, ability as TProjectPermission)
: children;
if (!isAllowed && passThrough) { if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>; return <Tooltip content={label}>{finalChild}</Tooltip>;
@@ -52,9 +52,11 @@ export enum ProjectPermissionSub {
Tags = "tags", Tags = "tags",
AuditLogs = "audit-logs", AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist", IpAllowList = "ip-allowlist",
Workspace = "workspace", Project = "workspace",
Secrets = "secrets", Secrets = "secrets",
SecretFolders = "secret-folders", SecretFolders = "secret-folders",
SecretImports = "secret-imports",
DynamicSecrets = "dynamic-secrets",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@@ -68,7 +70,24 @@ export enum ProjectPermissionSub {
Cmek = "cmek" Cmek = "cmek"
} }
type SubjectFields = { export type SecretSubjectFields = {
environment: string;
secretPath: string;
secretName: string;
secretTags: string[];
};
export type SecretFolderSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = {
environment: string;
secretPath: string;
};
export type SecretImportSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
}; };
@@ -76,13 +95,30 @@ type SubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields) (
| ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
)
] ]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields) | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.DynamicSecrets
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
) )
] ]
| [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Role]
@@ -95,19 +131,19 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]; | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>; export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
@@ -1,31 +1,29 @@
import { ComponentType } from "react"; import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; import { AbilityTuple } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons"; import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { TProjectPermission, useProjectPermission } from "@app/context"; import { useProjectPermission } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
type Props<T extends Abilities> = (T extends AbilityTuple type Props<T extends AbilityTuple> = {
? { className?: string;
action: T[0]; containerClassName?: string;
subject: Extract<T[1], SubjectType>; action: T[0];
} subject: T[1];
: { };
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
export const withProjectPermission = <T extends {}, J extends TProjectPermission>( export const withProjectPermission = <T extends {}>(
Component: ComponentType<T>, Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]> { action, subject, className, containerClassName }: Props<ProjectPermissionSet>
) => { ) => {
const HOC = (hocProps: T) => { const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
// akhilmhdh: Set as any due to casl/react ts type bug // akhilmhdh: Set as any due to casl/react ts type bug
// REASON: casl due to its type checking can't seem to union even if union intersection is applied // REASON: casl due to its type checking can't seem to union even if union intersection is applied
if (permission.cannot(action as any, subject)) { if (permission.cannot(action as any, subject as any)) {
return ( return (
<div <div
className={twMerge( className={twMerge(
+3 -3
View File
@@ -22,7 +22,7 @@ export const useCreateProjectRole = () => {
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => { mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
const { const {
data: { role } data: { role }
} = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto); } = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto);
return role; return role;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug }) => {
@@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => {
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => { mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
const { const {
data: { role } data: { role }
} = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto); } = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto);
return role; return role;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug }) => {
@@ -53,7 +53,7 @@ export const useDeleteProjectRole = () => {
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => { mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
const { const {
data: { role } data: { role }
} = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`); } = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`);
return role; return role;
}, },
onSuccess: (_, { projectSlug }) => { onSuccess: (_, { projectSlug }) => {
+17 -4
View File
@@ -7,6 +7,7 @@ import picomatch from "picomatch";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
import { omit } from "@app/lib/fn/object";
import { OrgUser, TProjectMembership } from "../users/types"; import { OrgUser, TProjectMembership } from "../users/types";
import { import {
@@ -49,7 +50,7 @@ export const roleQueryKeys = {
export const getProjectRoles = async (projectId: string) => { export const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>( const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v1/workspace/${projectId}/roles` `/api/v2/workspace/${projectId}/roles`
); );
return data.roles; return data.roles;
}; };
@@ -66,7 +67,7 @@ export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug), queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ role: TProjectRole }>( const { data } = await apiRequest.get<{ role: TProjectRole }>(
`/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}` `/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}`
); );
return data.role; return data.role;
}, },
@@ -134,7 +135,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]; permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] }; membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
}; };
}>(`/api/v1/workspace/${workspaceId}/permissions`, {}); }>(`/api/v2/workspace/${workspaceId}/permissions`, {});
return data.data; return data.data;
}; };
@@ -146,7 +147,19 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer
enabled: Boolean(workspaceId), enabled: Boolean(workspaceId),
select: (data) => { select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions); const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
const ability = createMongoAbility<ProjectPermissionSet>(rule, { conditionsMatcher }); const ability = createMongoAbility<ProjectPermissionSet>(rule, {
// this allows in frontend to skip some rules using *
conditionsMatcher: (rules) => {
return (entity) => {
const rulesStrippedOfWildcard = omit(
rules,
Object.keys(entity).filter((el) => entity[el].includes("*"))
);
const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard);
return baseMatcher(entity);
};
}
});
const membership = { const membership = {
...data.membership, ...data.membership,
+20
View File
@@ -0,0 +1,20 @@
/**
* Omit a list of properties from an object
* returning a new object with the properties
* that remain
*/
export const omit = <T, TKeys extends keyof T>(obj: T, keys: TKeys[]): Omit<T, TKeys> => {
if (!obj) return {} as Omit<T, TKeys>;
if (!keys || keys.length === 0) return obj as Omit<T, TKeys>;
return keys.reduce(
(acc, key) => {
// Gross, I know, it's mutating the object, but we
// are allowing it in this very limited scope due
// to the performance implications of an omit func.
// Not a pattern or practice to use elsewhere.
delete acc[key];
return acc;
},
{ ...obj }
);
};
@@ -28,10 +28,6 @@ const CmekPolicyActionSchema = z.object({
decrypt: z.boolean().optional() decrypt: z.boolean().optional()
}); });
const SecretFolderPolicyActionSchema = z.object({
read: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({ const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
create: z.boolean().optional() create: z.boolean().optional()
@@ -42,11 +38,29 @@ const WorkspacePolicyActionSchema = z.object({
delete: z.boolean().optional() delete: z.boolean().optional()
}); });
const ConditionSchema = z.object({ const ConditionSchema = z
operator: z.string(), .object({
lhs: z.string(), operator: z.string(),
rhs: z.string().min(1) lhs: z.string(),
}); rhs: z.string().min(1)
})
.array()
.optional()
.default([])
.refine(
(el) => {
const lhsOperatorSet = new Set<string>();
for (let i = 0; i < el.length; i += 1) {
const { lhs, operator } = el[i];
if (lhsOperatorSet.has(`${lhs}-${operator}`)) {
return false;
}
lhsOperatorSet.add(`${lhs}-${operator}`);
}
return true;
},
{ message: "Duplicate operator found for a condition" }
);
export const formSchema = z.object({ export const formSchema = z.object({
name: z.string().trim(), name: z.string().trim(),
@@ -59,27 +73,25 @@ export const formSchema = z.object({
permissions: z permissions: z
.object({ .object({
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
conditions: ConditionSchema.array() conditions: ConditionSchema
.optional() })
.default([]) .array()
.refine( .default([]),
(el) => { [ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({
const lhsOperatorSet = new Set<string>(); conditions: ConditionSchema
for (let i = 0; i < el.length; i += 1) { })
const { lhs, operator } = el[i]; .array()
if (lhsOperatorSet.has(`${lhs}-${operator}`)) { .default([]),
return false; [ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({
} conditions: ConditionSchema
lhsOperatorSet.add(`${lhs}-${operator}`); })
} .array()
return true; .default([]),
}, [ProjectPermissionSub.DynamicSecrets]: GeneralPolicyActionSchema.extend({
{ message: "Duplicate operator found for a condition" } conditions: ConditionSchema
)
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
@@ -98,7 +110,7 @@ export const formSchema = z.object({
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]), [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
@@ -110,8 +122,22 @@ export const formSchema = z.object({
export type TFormSchema = z.infer<typeof formSchema>; export type TFormSchema = z.infer<typeof formSchema>;
type TConditionalFields =
| ProjectPermissionSub.Secrets
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.DynamicSecrets;
export const isConditionalSubjects = (
subject: ProjectPermissionSub
): subject is TConditionalFields =>
subject === (ProjectPermissionSub.Secrets as const) ||
subject === ProjectPermissionSub.DynamicSecrets ||
subject === ProjectPermissionSub.SecretImports ||
subject === ProjectPermissionSub.SecretFolders;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema>[] = []; const formConditions: z.infer<typeof ConditionSchema> = [];
Object.entries(caslConditions).forEach(([type, condition]) => { Object.entries(caslConditions).forEach(([type, condition]) => {
if (typeof condition === "string") { if (typeof condition === "string") {
formConditions.push({ formConditions.push({
@@ -144,6 +170,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if ( if (
[ [
ProjectPermissionSub.Secrets, ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.Member, ProjectPermissionSub.Member,
ProjectPermissionSub.Groups, ProjectPermissionSub.Groups,
ProjectPermissionSub.Identity, ProjectPermissionSub.Identity,
@@ -172,7 +201,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canCreate = action.includes(ProjectPermissionActions.Create); const canCreate = action.includes(ProjectPermissionActions.Create);
// from above statement we are sure it won't be undefined // from above statement we are sure it won't be undefined
if (subject === ProjectPermissionSub.Secrets) { if (isConditionalSubjects(subject)) {
if (!formVal[subject]) formVal[subject] = []; if (!formVal[subject]) formVal[subject] = [];
formVal[subject]!.push({ formVal[subject]!.push({
read: canRead, read: canRead,
@@ -190,13 +219,13 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true; if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
} }
} else if (subject === ProjectPermissionSub.Workspace) { } else if (subject === ProjectPermissionSub.Project) {
const canEdit = action.includes(ProjectPermissionActions.Edit); const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete); const canDelete = action.includes(ProjectPermissionActions.Delete);
if (!formVal[subject]) formVal[subject] = [{}]; if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined // from above statement we are sure it won't be undefined
if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true; if (canEdit) formVal[subject as ProjectPermissionSub.Project]![0].edit = true;
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
} else if (subject === ProjectPermissionSub.SecretRollback) { } else if (subject === ProjectPermissionSub.SecretRollback) {
const canRead = action.includes(ProjectPermissionActions.Read); const canRead = action.includes(ProjectPermissionActions.Read);
@@ -206,12 +235,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
// from above statement we are sure it won't be undefined // from above statement we are sure it won't be undefined
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true; if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
} else if (subject === ProjectPermissionSub.SecretFolders) {
const canRead = action.includes(ProjectPermissionActions.Read);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
} else if (subject === ProjectPermissionSub.Cmek) { } else if (subject === ProjectPermissionSub.Cmek) {
const canRead = action.includes(ProjectPermissionCmekActions.Read); const canRead = action.includes(ProjectPermissionCmekActions.Read);
const canEdit = action.includes(ProjectPermissionCmekActions.Edit); const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
@@ -306,7 +329,30 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
}, },
[ProjectPermissionSub.SecretFolders]: { [ProjectPermissionSub.SecretFolders]: {
title: "Secret Folders", title: "Secret Folders",
actions: [{ label: "Read Only", value: "read" }] actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretImports]: {
title: "Secret Folders",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.DynamicSecrets]: {
title: "Secret Folders",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
}, },
[ProjectPermissionSub.Cmek]: { [ProjectPermissionSub.Cmek]: {
title: "KMS", title: "KMS",
@@ -332,7 +378,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove", value: "delete" } { label: "Remove", value: "delete" }
] ]
}, },
[ProjectPermissionSub.Workspace]: { [ProjectPermissionSub.Project]: {
title: "Project", title: "Project",
actions: [ actions: [
{ label: "Update project details", value: "edit" }, { label: "Update project details", value: "edit" },
@@ -10,13 +10,15 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions"; import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies";
import { NewPermissionRule } from "./components/NewPermissionRule"; import { NewPermissionRule } from "./components/NewPermissionRule";
import { SecretPermissionConditions } from "./components/SecretPermissionConditions"; import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState"; import { PermissionEmptyState } from "./PermissionEmptyState";
import { import {
formRolePermission2API, formRolePermission2API,
formSchema, formSchema,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
rolePermission2Form, rolePermission2Form,
TFormSchema TFormSchema
@@ -27,6 +29,17 @@ type Props = {
isDisabled?: boolean; isDisabled?: boolean;
}; };
const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: boolean) => {
if (subject === ProjectPermissionSub.Secrets)
return <SecretPermissionConditions isDisabled={isDisabled} />;
if (isConditionalSubjects(subject)) {
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}
return undefined;
};
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
@@ -130,17 +143,15 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
<div className="py-4"> <div className="py-4">
{!isLoading && <PermissionEmptyState />} {!isLoading && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
<GeneralPermissionOptions <GeneralPermissionPolicies
subject={subject} subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions} actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title} title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`} key={`project-permission-${subject}`}
isDisabled={isDisabled} isDisabled={isDisabled}
> >
{subject === ProjectPermissionSub.Secrets ? ( {renderConditionalComponents(subject, isDisabled)}
<SecretPermissionConditions isDisabled={isDisabled} /> </GeneralPermissionPolicies>
) : undefined}
</GeneralPermissionOptions>
))} ))}
</div> </div>
</FormProvider> </FormProvider>
@@ -0,0 +1,155 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { TFormSchema } from "../ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
type:
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports;
};
const getValueLabel = (type: string) => {
if (type === "environment") return "Environment slug";
if (type === "secretPath") return "Folder path";
return "";
};
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${type}.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-gray-800 bg-mineshaft-800 pt-2">
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const lhs = watch(`permissions.${type}.${position}.conditions.${index}.lhs`);
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="w-36">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>
Glob Match
</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} placeholder={getValueLabel(lhs)} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[type]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[type]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
New Condition
</Button>
</div>
</div>
);
};
@@ -8,7 +8,11 @@ import { Button, Checkbox, Tag } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils"; import {
isConditionalSubjects,
TFormSchema,
TProjectPermissionObject
} from "../ProjectRoleModifySection.utils";
type Props<T extends ProjectPermissionSub> = { type Props<T extends ProjectPermissionSub> = {
title: string; title: string;
@@ -18,7 +22,7 @@ type Props<T extends ProjectPermissionSub> = {
isDisabled?: boolean; isDisabled?: boolean;
}; };
export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema["permissions"]>>({ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
subject, subject,
actions, actions,
children, children,
@@ -98,10 +102,10 @@ export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema
<div <div
className={twMerge( className={twMerge(
"mt-4 flex justify-start space-x-4", "mt-4 flex justify-start space-x-4",
subject === ProjectPermissionSub.Secrets && "justify-end" isConditionalSubjects(subject) && "justify-end"
)} )}
> >
{!isDisabled && subject === ProjectPermissionSub.Secrets && ( {!isDisabled && isConditionalSubjects(subject) && (
<Button <Button
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star" variant="star"
@@ -15,6 +15,7 @@ import { ProjectPermissionSub } from "@app/context";
import { import {
formSchema, formSchema,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
TFormSchema TFormSchema
} from "../ProjectRoleModifySection.utils"; } from "../ProjectRoleModifySection.utils";
@@ -89,7 +90,7 @@ export const NewPermissionRule = ({ onClose }: Props) => {
<Button <Button
onClick={form.handleSubmit((el) => { onClick={form.handleSubmit((el) => {
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type]; const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
if (rootPolicyValue && selectedSubject === ProjectPermissionSub.Secrets) { if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
rootForm.setValue( rootForm.setValue(
`permissions.${el.type}`, `permissions.${el.type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -79,8 +79,23 @@ export const SecretMainPage = () => {
const secretPath = (router.query.secretPath as string) || "/"; const secretPath = (router.query.secretPath as string) || "/";
const canReadSecret = permission.can( const canReadSecret = permission.can(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})
); );
const canReadSecretImports = permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
);
const canReadDynamicSecret = permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
);
const canDoReadRollback = permission.can( const canDoReadRollback = permission.can(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
@@ -91,8 +106,8 @@ export const SecretMainPage = () => {
searchFilter: (router.query.searchFilter as string) || "", searchFilter: (router.query.searchFilter as string) || "",
include: { include: {
[RowType.Folder]: true, [RowType.Folder]: true,
[RowType.Import]: canReadSecret, [RowType.Import]: canReadSecretImports,
[RowType.DynamicSecret]: canReadSecret, [RowType.DynamicSecret]: canReadDynamicSecret,
[RowType.Secret]: canReadSecret [RowType.Secret]: canReadSecret
} }
}; };
@@ -107,12 +122,12 @@ export const SecretMainPage = () => {
...prev, ...prev,
include: { include: {
[RowType.Folder]: true, [RowType.Folder]: true,
[RowType.Import]: canReadSecret, [RowType.Import]: canReadSecretImports,
[RowType.DynamicSecret]: canReadSecret, [RowType.DynamicSecret]: canReadDynamicSecret,
[RowType.Secret]: canReadSecret [RowType.Secret]: canReadSecret
} }
})); }));
}, [canReadSecret]); }, [canReadSecret, canReadSecretImports, canReadDynamicSecret]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -141,9 +156,9 @@ export const SecretMainPage = () => {
orderBy, orderBy,
search: debouncedSearchFilter, search: debouncedSearchFilter,
orderDirection, orderDirection,
includeImports: canReadSecret && filter.include.import, includeImports: canReadSecretImports && filter.include.import,
includeFolders: filter.include.folder, includeFolders: filter.include.folder,
includeDynamicSecrets: canReadSecret && filter.include.dynamic, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
includeSecrets: canReadSecret && filter.include.secret, includeSecrets: canReadSecret && filter.include.secret,
tags: filter.tags tags: filter.tags
}); });
@@ -362,7 +377,7 @@ export const SecretMainPage = () => {
<div className="flex-grow px-4 py-2">Value</div> <div className="flex-grow px-4 py-2">Value</div>
</div> </div>
)} )}
{canReadSecret && imports?.length && ( {canReadSecretImports && imports?.length && (
<SecretImportListView <SecretImportListView
searchTerm={debouncedSearchFilter} searchTerm={debouncedSearchFilter}
secretImports={imports} secretImports={imports}
@@ -382,7 +397,7 @@ export const SecretMainPage = () => {
onNavigateToFolder={handleResetFilter} onNavigateToFolder={handleResetFilter}
/> />
)} )}
{canReadSecret && dynamicSecrets?.length && ( {canReadDynamicSecret && dynamicSecrets?.length && (
<DynamicSecretListView <DynamicSecretListView
environment={environment} environment={environment}
projectSlug={projectSlug} projectSlug={projectSlug}
@@ -401,7 +416,10 @@ export const SecretMainPage = () => {
isProtectedBranch={isProtectedBranch} isProtectedBranch={isProtectedBranch}
/> />
)} )}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />} {!canReadSecret &&
!canReadDynamicSecret &&
!canReadSecretImports &&
folders?.length === 0 && <PermissionDeniedBanner />}
</div> </div>
</div> </div>
{!isDetailsLoading && totalCount > 0 && ( {!isDetailsLoading && totalCount > 0 && (
@@ -45,12 +45,7 @@ import {
Tooltip, Tooltip,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription
} from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
@@ -125,12 +120,6 @@ export const ActionBar = ({
const { reset: resetSelectedSecret } = useSelectedSecretActions(); const { reset: resetSelectedSecret } = useSelectedSecretActions();
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
const { permission } = useProjectPermission();
const shouldCheckFolderPermission = permission.rules.some((rule) =>
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
);
const handleFolderCreate = async (folderName: string) => { const handleFolderCreate = async (folderName: string) => {
try { try {
await createFolder({ await createFolder({
@@ -438,7 +427,12 @@ export const ActionBar = ({
<div className="flex items-center"> <div className="flex items-center">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -469,12 +463,7 @@ export const ActionBar = ({
<div className="flex flex-col space-y-1 p-1.5"> <div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject( a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -494,7 +483,12 @@ export const ActionBar = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -518,7 +512,10 @@ export const ActionBar = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.SecretImports, {
environment,
secretPath
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -558,7 +555,12 @@ export const ActionBar = ({
</div> </div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
renderTooltip renderTooltip
allowedLabel="Move" allowedLabel="Move"
> >
@@ -577,7 +579,12 @@ export const ActionBar = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
renderTooltip renderTooltip
allowedLabel="Delete" allowedLabel="Delete"
> >
@@ -60,7 +60,6 @@ export const DynamicSecretLease = ({
path: secretPath, path: secretPath,
dynamicSecretName dynamicSecretName
}); });
const deleteDynamicSecretLease = useRevokeDynamicSecretLease(); const deleteDynamicSecretLease = useRevokeDynamicSecretLease();
@@ -141,7 +140,7 @@ export const DynamicSecretLease = ({
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip renderTooltip
allowedLabel="Renew" allowedLabel="Renew"
> >
@@ -160,7 +159,7 @@ export const DynamicSecretLease = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip renderTooltip
allowedLabel="Delete" allowedLabel="Delete"
> >
@@ -180,7 +179,10 @@ export const DynamicSecretLease = ({
{status === DynamicSecretLeaseStatus.FailedDeletion && ( {status === DynamicSecretLeaseStatus.FailedDeletion && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.DynamicSecrets, {
environment,
secretPath
})}
renderTooltip renderTooltip
allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems." allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems."
> >
@@ -166,7 +166,7 @@ export const DynamicSecretListView = ({
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3"> <div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip renderTooltip
allowedLabel="Edit" allowedLabel="Edit"
> >
@@ -188,7 +188,7 @@ export const DynamicSecretListView = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip renderTooltip
allowedLabel="Delete" allowedLabel="Delete"
> >
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2"; import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api"; import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -33,11 +33,6 @@ export const FolderListView = ({
"deleteFolder" "deleteFolder"
] as const); ] as const);
const router = useRouter(); const router = useRouter();
const { permission } = useProjectPermission();
const shouldCheckFolderPermission = permission.rules.some((rule) =>
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
);
const { mutateAsync: updateFolder } = useUpdateFolder(); const { mutateAsync: updateFolder } = useUpdateFolder();
const { mutateAsync: deleteFolder } = useDeleteFolder(); const { mutateAsync: deleteFolder } = useDeleteFolder();
@@ -126,12 +121,7 @@ export const FolderListView = ({
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3"> <div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject( a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
renderTooltip renderTooltip
allowedLabel="Edit" allowedLabel="Edit"
> >
@@ -150,12 +140,7 @@ export const FolderListView = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject( a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
renderTooltip renderTooltip
allowedLabel="Delete" allowedLabel="Delete"
> >
@@ -142,7 +142,12 @@ export const CopySecretsFromBoard = ({
<div> <div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -250,7 +250,12 @@ export const SecretDropzone = ({
</div> </div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<input <input
@@ -287,7 +292,12 @@ export const SecretDropzone = ({
{!isSmaller && ( {!isSmaller && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -67,7 +67,7 @@ export const SecretImportItem = ({
isReplicationExpand, isReplicationExpand,
importedSecrets = [], importedSecrets = [],
searchTerm = "", searchTerm = "",
secretPath, secretPath = "/",
environment, environment,
secretImport, secretImport,
onExpandReplicateSecrets: onExpandReplicate onExpandReplicateSecrets: onExpandReplicate
@@ -209,7 +209,7 @@ export const SecretImportItem = ({
{isReplication && ( {isReplication && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
renderTooltip renderTooltip
allowedLabel="Resync replicated secrets" allowedLabel="Resync replicated secrets"
> >
@@ -235,7 +235,10 @@ export const SecretImportItem = ({
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2"> <div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.SecretImports, {
environment,
secretPath: secretPath || "/"
})}
renderTooltip renderTooltip
allowedLabel="Change order" allowedLabel="Change order"
> >
@@ -256,7 +259,7 @@ export const SecretImportItem = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
renderTooltip renderTooltip
allowedLabel="Delete" allowedLabel="Delete"
> >
@@ -85,15 +85,6 @@ export const SecretDetailSidebar = ({
values: secret values: secret
}); });
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) && cannotEditSecret;
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
@@ -104,6 +95,27 @@ export const SecretDetailSidebar = ({
(prev, curr) => ({ ...prev, [curr.id]: true }), (prev, curr) => ({ ...prev, [curr.id]: true }),
{} {}
); );
const selectTagSlugs = selectedTags.map((i) => i.slug);
const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})
);
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})
) && cannotEditSecret;
const overrideAction = watch("overrideAction"); const overrideAction = watch("overrideAction");
const isOverridden = const isOverridden =
@@ -194,7 +206,12 @@ export const SecretDetailSidebar = ({
</FormControl> </FormControl>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Controller <Controller
@@ -221,7 +238,12 @@ export const SecretDetailSidebar = ({
<div className="mb-2 border-b border-mineshaft-600 pb-4"> <div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Switch <Switch
@@ -277,7 +299,12 @@ export const SecretDetailSidebar = ({
<DropdownMenu> <DropdownMenu>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -388,7 +415,12 @@ export const SecretDetailSidebar = ({
render={({ field: { value, onChange, onBlur } }) => ( render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Switch <Switch
@@ -450,7 +482,12 @@ export const SecretDetailSidebar = ({
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -465,7 +502,12 @@ export const SecretDetailSidebar = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: selectTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}> <Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
@@ -81,15 +81,6 @@ export const SecretItem = memo(
}: Props) => { }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const { const {
handleSubmit, handleSubmit,
@@ -107,6 +98,8 @@ export const SecretItem = memo(
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const secretName = watch("key");
const secretReminderRepeatDays = watch("reminderRepeatDays"); const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote"); const secretReminderNote = watch("reminderNote");
@@ -118,11 +111,33 @@ export const SecretItem = memo(
(prev, curr) => ({ ...prev, [curr.id]: true }), (prev, curr) => ({ ...prev, [curr.id]: true }),
{} {}
); );
const selectedTagSlugs = selectedTags.map((i) => i.slug);
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
name: "tags" name: "tags"
}); });
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})
);
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false); const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
useEffect(() => { useEffect(() => {
@@ -309,7 +324,12 @@ export const SecretItem = memo(
<DropdownMenu> <DropdownMenu>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuTrigger asChild disabled={!isAllowed}> <DropdownMenuTrigger asChild disabled={!isAllowed}>
@@ -384,7 +404,12 @@ export const SecretItem = memo(
</DropdownMenu> </DropdownMenu>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
renderTooltip renderTooltip
allowedLabel="Override" allowedLabel="Override"
> >
@@ -440,7 +465,12 @@ export const SecretItem = memo(
<Popover> <Popover>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<PopoverTrigger asChild disabled={!isAllowed}> <PopoverTrigger asChild disabled={!isAllowed}>
@@ -519,7 +549,12 @@ export const SecretItem = memo(
</Tooltip> </Tooltip>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
renderTooltip renderTooltip
allowedLabel="Delete" allowedLabel="Delete"
> >
@@ -303,7 +303,7 @@ export const SecretOverviewPage = () => {
if ( if (
permission.can( permission.can(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath }) subject(ProjectPermissionSub.SecretFolders, { environment: env.slug, secretPath })
) )
) { ) {
const folder = getFolderByNameAndEnv(oldFolderName, env.slug); const folder = getFolderByNameAndEnv(oldFolderName, env.slug);
@@ -506,20 +506,13 @@ export const SecretOverviewPage = () => {
const pathSegment = secretPath.split("/").filter(Boolean); const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`; const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1); const folderName = pathSegment.at(-1);
const canCreateFolder = permission.rules.some((rule) => const canCreateFolder = permission.can(
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) ProjectPermissionActions.Create,
) subject(ProjectPermissionSub.SecretFolders, {
? permission.can( environment: slug,
ProjectPermissionActions.Create, secretPath: parentPath
subject(ProjectPermissionSub.SecretFolders, { })
environment: slug, );
secretPath: parentPath
})
)
: permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: parentPath })
);
if (folderName && parentPath && canCreateFolder) { if (folderName && parentPath && canCreateFolder) {
await createFolder({ await createFolder({
projectId: workspaceId, projectId: workspaceId,
@@ -771,7 +764,7 @@ export const SecretOverviewPage = () => {
<div className="flex flex-col space-y-1 p-1.5"> <div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })} a={ProjectPermissionSub.SecretFolders}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -93,23 +93,14 @@ export const CreateSecretForm = ({
const pathSegment = secretPath.split("/").filter(Boolean); const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`; const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1); const folderName = pathSegment.at(-1);
const canCreateFolder = permission.rules.some((rule) => const canCreateFolder = permission.can(
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) ProjectPermissionActions.Create,
) subject(ProjectPermissionSub.SecretFolders, {
? permission.can( environment: env.slug,
ProjectPermissionActions.Create, secretPath: parentPath
subject(ProjectPermissionSub.SecretFolders, { })
environment: env.slug, );
secretPath: parentPath
})
)
: permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath: parentPath
})
);
if (folderName && parentPath && canCreateFolder) { if (folderName && parentPath && canCreateFolder) {
await createFolder({ await createFolder({
projectId: workspaceId, projectId: workspaceId,
@@ -250,7 +241,9 @@ export const CreateSecretForm = ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment: environmentSlug.slug, environment: environmentSlug.slug,
secretPath secretPath,
secretName: "*",
secretTags: ["*"]
}) })
) )
) )
@@ -1,4 +1,4 @@
import { useCallback,useState } from "react"; import { useCallback, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
@@ -7,7 +7,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { DeleteActionModal,IconButton, Tooltip } from "@app/components/v2"; import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
@@ -63,8 +63,8 @@ export const SecretEditRow = ({
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const toggleModal = useCallback(() => { const toggleModal = useCallback(() => {
setIsModalOpen((prev) => !prev) setIsModalOpen((prev) => !prev);
}, []) }, []);
const handleFormReset = () => { const handleFormReset = () => {
reset(); reset();
@@ -114,7 +114,6 @@ export const SecretEditRow = ({
return ( return (
<div className="group flex w-full cursor-text items-center space-x-2"> <div className="group flex w-full cursor-text items-center space-x-2">
<DeleteActionModal <DeleteActionModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={toggleModal} onClose={toggleModal}
@@ -151,8 +150,13 @@ export const SecretEditRow = ({
{isDirty ? ( {isDirty ? (
<> <>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={isCreatable ? ProjectPermissionActions.Create : ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: ["*"]
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<div> <div>
@@ -18,7 +18,7 @@ import {
} from "@app/context"; } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { useUpdateSecretV3 } from "@app/hooks/api"; import { useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils"; import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
type Props = { type Props = {
@@ -42,15 +42,16 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
const isReadOnly = environments.some((env) => { const isReadOnly = environments.some((env) => {
const environment = env.slug; const environment = env.slug;
const secretDetails = getSecretByKey(environment, secretKey);
const secretPermissionSubject = subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
});
const isSecretInEnvReadOnly = const isSecretInEnvReadOnly =
permission.can( permission.can(ProjectPermissionActions.Read, secretPermissionSubject) &&
ProjectPermissionActions.Read, permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject);
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
if (isSecretInEnvReadOnly) { if (isSecretInEnvReadOnly) {
return true; return true;
} }
@@ -49,9 +49,9 @@ export const SelectionPanel = ({
"bulkDeleteEntries" "bulkDeleteEntries"
] as const); ] as const);
const selectedFolderCount = Object.keys(selectedEntries.folder).length const selectedFolderCount = Object.keys(selectedEntries.folder).length;
const selectedKeysCount = Object.keys(selectedEntries.secret).length const selectedKeysCount = Object.keys(selectedEntries.secret).length;
const selectedCount = selectedFolderCount + selectedKeysCount const selectedCount = selectedFolderCount + selectedKeysCount;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
@@ -65,7 +65,12 @@ export const SelectionPanel = ({
const shouldShowDelete = userAvailableEnvs.some((env) => const shouldShowDelete = userAvailableEnvs.some((env) =>
permission.can( permission.can(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath }) subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath,
secretName: "*",
secretTags: ["*"]
})
) )
); );
@@ -77,41 +82,50 @@ export const SelectionPanel = ({
return "Do you want to delete the selected secrets across environments?"; return "Do you want to delete the selected secrets across environments?";
} }
return "Do you want to delete the selected folders across environments?"; return "Do you want to delete the selected folders across environments?";
} };
const handleBulkDelete = async () => { const handleBulkDelete = async () => {
let processedEntries = 0; let processedEntries = 0;
const promises = userAvailableEnvs.map(async (env) => { const promises = userAvailableEnvs.map(async (env) => {
// additional check: ensure that bulk delete is only executed on envs that user has access to // additional check: ensure that bulk delete is only executed on envs that user has access to
if ( if (
permission.cannot( permission.can(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath }) subject(ProjectPermissionSub.SecretFolders, { environment: env.slug, secretPath })
) )
) { ) {
return; await Promise.all(
Object.keys(selectedEntries.folder).map(async (folderName) => {
const folder = getFolderByNameAndEnv(folderName, env.slug);
if (folder) {
processedEntries += 1;
await deleteFolder({
folderId: folder?.id,
path: secretPath,
environment: env.slug,
projectId: workspaceId
});
}
})
);
} }
await Promise.all(
Object.keys(selectedEntries.folder).map(async (folderName) => {
const folder = getFolderByNameAndEnv(folderName, env.slug);
if (folder) {
processedEntries += 1;
await deleteFolder({
folderId: folder?.id,
path: secretPath,
environment: env.slug,
projectId: workspaceId
});
}
})
);
const secretsToDelete = Object.keys(selectedEntries.secret).reduce( const secretsToDelete = Object.keys(selectedEntries.secret).reduce(
(accum: TDeleteSecretBatchDTO["secrets"], secretName) => { (accum: TDeleteSecretBatchDTO["secrets"], secretName) => {
const entry = getSecretByKey(env.slug, secretName); const entry = getSecretByKey(env.slug, secretName);
if (entry) { const canDeleteSecret = permission.can(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath,
secretName,
secretTags: (entry?.tags || []).map((i) => i.slug)
})
);
if (entry && canDeleteSecret) {
return [ return [
...accum, ...accum,
{ {
@@ -136,10 +136,7 @@ export const DeleteProjectSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p> <p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
<div className="space-x-4"> <div className="space-x-4">
<ProjectPermissionCan <ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Workspace}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
isLoading={isDeleting} isLoading={isDeleting}
@@ -318,10 +318,7 @@ export const EncryptionTab = () => {
/> />
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Workspace}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
@@ -22,7 +22,6 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>; type FormData = yup.InferType<typeof formSchema>;
export const ProjectNameChangeSection = () => { export const ProjectNameChangeSection = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useRenameWorkspace(); const { mutateAsync, isLoading } = useRenameWorkspace();
@@ -83,7 +82,7 @@ export const ProjectNameChangeSection = () => {
</div> </div>
</div> </div>
<div className="max-w-md"> <div className="max-w-md">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
{(isAllowed) => ( {(isAllowed) => (
<Controller <Controller
defaultValue="" defaultValue=""
@@ -103,7 +102,7 @@ export const ProjectNameChangeSection = () => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}> <ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" colorSchema="secondary"