Revert "Revert "Permission phase 2""

This reverts commit 8b9244b079.
This commit is contained in:
=
2024-10-18 16:03:36 +05:30
parent 45b9de63f0
commit 93218d5a3f
100 changed files with 3390 additions and 1615 deletions

View File

@@ -0,0 +1,33 @@
import { useState } from "react";
import picomatch from "picomatch";
import { FormControl } from "../v2/FormControl";
import { Input } from "../v2/Input";
export const GlobPermissionInfo = () => {
const [pattern, setPattern] = useState("");
const [text, setText] = useState("");
return (
<div>
<div className="mt-2">A glob pattern uses wildcards to match resources or paths.</div>
<div>
<FormControl label="Glob pattern" helperText="Examples: /{a,b}, DB_**">
<Input value={pattern} onChange={(e) => setPattern(e.target.value)} />
</FormControl>
</div>
<div>
<FormControl
label="Test string"
helperText="Type a value to test glob match"
isError={
pattern && text ? !picomatch.isMatch(text, pattern, { strictSlashes: false }) : false
}
errorText="Invalid"
>
<Input value={text} onChange={(e) => setText(e.target.value)} />
</FormControl>
</div>
</div>
);
};

View File

@@ -1,23 +1,25 @@
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;
// 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
renderTooltip?: boolean;
allowedLabel?: string;
// BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission
// For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error
// tried a lot combinations
// REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15
} & BoundCanProps<any>;
children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
passThrough?: boolean;
I: T[0];
a: T[1];
ability?: MongoAbility<T>;
};
export const ProjectPermissionCan: FunctionComponent<Props> = ({
export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>> = ({
label = "Access restricted",
children,
passThrough = true,
@@ -31,9 +33,7 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
{(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type.
const finalChild =
typeof children === "function"
? children(isAllowed, ability as TProjectPermission)
: children;
typeof children === "function" ? children(isAllowed, ability as any) : children;
if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>;

View File

@@ -1,3 +1,4 @@
export { GlobPermissionInfo } from "./GlobPermissionInfo";
export { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan";

View File

@@ -12,6 +12,7 @@ type Props = {
placeholder?: string;
className?: string;
dropdownContainerClassName?: string;
containerClassName?: string;
isLoading?: boolean;
position?: "item-aligned" | "popper";
isDisabled?: boolean;
@@ -31,12 +32,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
isDisabled,
dropdownContainerClassName,
position,
containerClassName,
...props
},
ref
): JSX.Element => {
return (
<div className="flex items-center space-x-2">
<div className={twMerge("flex items-center space-x-2", containerClassName)}>
<SelectPrimitive.Root
{...props}
onValueChange={(value) => {

View File

@@ -3,5 +3,6 @@ export type { ProjectPermissionSet, TProjectPermission } from "./types";
export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub
} from "./types";

View File

@@ -7,6 +7,14 @@ export enum ProjectPermissionActions {
Delete = "delete"
}
export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential",
EditRootCredential = "edit-root-credential",
DeleteRootCredential = "delete-root-credential",
Lease = "lease"
}
export enum ProjectPermissionCmekActions {
Read = "read",
Create = "create",
@@ -21,7 +29,7 @@ export enum PermissionConditionOperators {
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$neq",
$NEQ = "$ne",
$GLOB = "$glob"
}
@@ -37,7 +45,7 @@ export type TPermissionConditionOperators = {
export type TPermissionCondition = Record<
string,
| string
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string }
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string }
>;
export enum ProjectPermissionSub {
@@ -52,9 +60,11 @@ export enum ProjectPermissionSub {
Tags = "tags",
AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist",
Workspace = "workspace",
Project = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretImports = "secret-imports",
DynamicSecrets = "dynamic-secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
@@ -68,7 +78,24 @@ export enum ProjectPermissionSub {
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;
secretPath: string;
};
@@ -76,13 +103,30 @@ type SubjectFields = {
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
(
| ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretFolders
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
)
]
| [
ProjectPermissionDynamicSecretActions,
(
| ProjectPermissionSub.DynamicSecrets
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
@@ -95,19 +139,19 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek];
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -11,6 +11,7 @@ export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionProvider,
ProjectPermissionSub,
useProjectPermission

View File

@@ -1,31 +1,29 @@
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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
? {
action: T[0];
subject: Extract<T[1], SubjectType>;
}
: {
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
type Props<T extends AbilityTuple> = {
className?: string;
containerClassName?: string;
action: T[0];
subject: T[1];
};
export const withProjectPermission = <T extends {}, J extends TProjectPermission>(
Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
export const withProjectPermission = <T extends {}>(
Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>,
{ action, subject, className, containerClassName }: Props<ProjectPermissionSet>
) => {
const HOC = (hocProps: T) => {
const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => {
const { permission } = useProjectPermission();
// 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
if (permission.cannot(action as any, subject)) {
if (permission.cannot(action as any, subject as any)) {
return (
<div
className={twMerge(

View File

@@ -15,6 +15,7 @@ import {
} from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { unique } from "@app/lib/fn/array";
export const dashboardKeys = {
all: () => ["dashboard"] as const,
@@ -154,10 +155,20 @@ export const useGetProjectSecretsOverview = (
},
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
const { secrets, ...select } = data;
const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
const uniqueFolders = select.folders ? unique(select.folders, (i) => i.name) : [];
const uniqueDynamicSecrets = select.dynamicSecrets
? unique(select.dynamicSecrets, (i) => i.name)
: [];
return {
...select,
secrets: secrets ? mergePersonalSecrets(secrets) : undefined
secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
totalUniqueSecretsInPage: uniqueSecrets.length,
totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
totalUniqueFoldersInPage: uniqueFolders.length
};
}, []),
keepPreviousData: true

View File

@@ -12,6 +12,9 @@ export type DashboardProjectSecretsOverviewResponse = {
totalFolderCount?: number;
totalDynamicSecretCount?: number;
totalCount: number;
totalUniqueSecretsInPage: number;
totalUniqueDynamicSecretsInPage: number;
totalUniqueFoldersInPage: number;
};
export type DashboardProjectSecretsDetailsResponse = {

View File

@@ -1,4 +1,3 @@
import { packRules } from "@casl/ability/extra";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
@@ -16,10 +15,7 @@ export const useCreateProjectUserAdditionalPrivilege = () => {
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", {
...dto,
permissions: packRules(dto.permissions)
});
const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto);
return data.privilege;
},
onSuccess: (_, { projectMembershipId }) => {
@@ -35,7 +31,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => {
mutationFn: async (dto) => {
const { data } = await apiRequest.patch(
`/api/v1/additional-privilege/users/${dto.privilegeId}`,
{ ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined }
dto
);
return data.privilege;
},

View File

@@ -1,4 +1,3 @@
import { PackRule, unpackRules } from "@casl/ability/extra";
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
@@ -18,10 +17,7 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => {
} = await apiRequest.get<{
privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown };
}>(`/api/v1/additional-privilege/users/${privilegeId}`);
return {
...privilege,
permissions: unpackRules(privilege.permissions as PackRule<TProjectPermission>[])
};
return privilege;
};
export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => {
@@ -44,7 +40,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => {
}>("/api/v1/additional-privilege/users", { params: { projectMembershipId } });
return privileges.map((el) => ({
...el,
permissions: unpackRules(el.permissions as PackRule<TProjectPermission>[])
permissions: el.permissions as TProjectPermission[]
}));
}
});

View File

@@ -4,6 +4,15 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TProjectSpecificPrivilegePermission = {
conditions: {
environment: string;
secretPath?: { $glob: string };
};
actions: string[];
subject: string;
};
export type TProjectUserPrivilege = {
projectMembershipId: string;
slug: string;
@@ -12,21 +21,21 @@ export type TProjectUserPrivilege = {
updatedAt: Date;
permissions?: TProjectPermission[];
} & (
| {
| {
isTemporary: true;
temporaryMode: string;
temporaryRange: string;
temporaryAccessStartTime: string;
temporaryAccessEndTime?: string;
}
| {
| {
isTemporary: false;
temporaryMode?: null;
temporaryRange?: null;
temporaryAccessStartTime?: null;
temporaryAccessEndTime?: null;
}
);
);
export type TCreateProjectUserPrivilegeDTO = {
projectMembershipId: string;
@@ -35,7 +44,7 @@ export type TCreateProjectUserPrivilegeDTO = {
temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode;
temporaryRange?: string;
temporaryAccessStartTime?: string;
permissions: TProjectPermission[];
permissions: TProjectSpecificPrivilegePermission;
};
export type TUpdateProjectUserPrivlegeDTO = {

View File

@@ -22,7 +22,7 @@ export const useCreateProjectRole = () => {
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto);
} = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto);
return role;
},
onSuccess: (_, { projectSlug }) => {
@@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => {
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto);
} = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto);
return role;
},
onSuccess: (_, { projectSlug }) => {
@@ -53,7 +53,7 @@ export const useDeleteProjectRole = () => {
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`);
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`);
return role;
},
onSuccess: (_, { projectSlug }) => {

View File

@@ -7,6 +7,8 @@ import picomatch from "picomatch";
import { apiRequest } from "@app/config/request";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
import { groupBy } from "@app/lib/fn/array";
import { omit } from "@app/lib/fn/object";
import { OrgUser, TProjectMembership } from "../users/types";
import {
@@ -49,7 +51,7 @@ export const roleQueryKeys = {
export const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v1/workspace/${projectId}/roles`
`/api/v2/workspace/${projectId}/roles`
);
return data.roles;
};
@@ -66,7 +68,7 @@ export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
queryFn: async () => {
const { data } = await apiRequest.get<{ role: TProjectRole }>(
`/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}`
`/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}`
);
return data.role;
},
@@ -134,7 +136,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
};
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
}>(`/api/v2/workspace/${workspaceId}/permissions`, {});
return data.data;
};
@@ -146,8 +148,32 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer
enabled: Boolean(workspaceId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
const ability = createMongoAbility<ProjectPermissionSet>(rule, { conditionsMatcher });
const negatedRules = groupBy(
rule.filter((i) => i.inverted && i.conditions),
(i) => `${i.subject}-${JSON.stringify(i.conditions)}`
);
const ability = createMongoAbility<ProjectPermissionSet>(rule, {
// this allows in frontend to skip some rules using *
conditionsMatcher: (rules) => {
return (entity) => {
// skip validation if its negated rules
const isNegatedRule =
// eslint-disable-next-line no-underscore-dangle
negatedRules?.[`${entity.__caslSubjectType__}-${JSON.stringify(rules)}`];
if (isNegatedRule) {
const baseMatcher = conditionsMatcher(rules);
return baseMatcher(entity);
}
const rulesStrippedOfWildcard = omit(
rules,
Object.keys(entity).filter((el) => entity[el]?.includes("*"))
);
const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard);
return baseMatcher(entity);
};
}
});
const membership = {
...data.membership,
roles: data.membership.roles.map(({ role }) => role)

View File

@@ -40,6 +40,7 @@ export type TPermission = {
export type TProjectPermission = {
conditions?: Record<string, any>;
inverted?: boolean;
action: string | string[];
subject: string | string[];
};

View File

@@ -13,3 +13,22 @@ export const groupBy = <T, Key extends string | number | symbol>(
acc[groupId].push(item);
return acc;
}, {} as Record<Key, T[]>);
/**
* Given a list of items returns a new list with only
* unique items. Accepts an optional identity function
* to convert each item in the list to a comparable identity
* value
*/
export const unique = <T, K extends string | number | symbol>(
array: readonly T[],
toKey?: (item: T) => K
): T[] => {
const valueMap = array.reduce((acc, item) => {
const key = toKey ? toKey(item) : (item as unknown as string | number | symbol);
if (acc[key]) return acc;
acc[key] = item;
return acc;
}, {} as Record<string | number | symbol, T>);
return Object.values(valueMap);
};

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 }
);
};

View File

@@ -184,20 +184,20 @@ export const SpecificPrivilegeSecretForm = ({
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
];
const conditions: Record<string, any> = { environment: data.environmentSlug };
const conditions: { environment: string; secretPath?: { $glob: string } } = {
environment: data.environmentSlug
};
if (data.secretPath) {
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
}
await updateUserPrivilege.mutateAsync({
privilegeId: privilege.id,
...data.temporaryAccess,
permissions: actions
.filter(({ allowed }) => allowed)
.map(({ action }) => ({
action,
subject: [ProjectPermissionSub.Secrets],
conditions
})),
permissions: {
subject: ProjectPermissionSub.Secrets,
conditions,
actions: actions.filter((i) => i.allowed).map((i) => i.action)
},
projectMembershipId: privilege.projectMembershipId
});
createNotification({
@@ -642,15 +642,13 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => {
if (createUserPrivilege.isLoading) return;
try {
await createUserPrivilege.mutateAsync({
permissions: [
{
action: ProjectPermissionActions.Read,
subject: [ProjectPermissionSub.Secrets],
conditions: {
environment: currentWorkspace?.environments?.[0].slug
}
permissions: {
actions: [ProjectPermissionActions.Read],
subject: ProjectPermissionSub.Secrets,
conditions: {
environment: currentWorkspace?.environments?.[0].slug || ""
}
],
},
projectMembershipId: membershipId
});
createNotification({

View File

@@ -7,6 +7,7 @@ import {
} from "@app/context";
import {
PermissionConditionOperators,
ProjectPermissionDynamicSecretActions,
TPermissionCondition,
TPermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types";
@@ -28,8 +29,12 @@ const CmekPolicyActionSchema = z.object({
decrypt: z.boolean().optional()
});
const SecretFolderPolicyActionSchema = z.object({
read: z.boolean().optional()
const DynamicSecretPolicyActionSchema = z.object({
[ProjectPermissionDynamicSecretActions.ReadRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.EditRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.DeleteRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.CreateRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.Lease]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({
@@ -42,11 +47,29 @@ const WorkspacePolicyActionSchema = z.object({
delete: z.boolean().optional()
});
const ConditionSchema = z.object({
operator: z.string(),
lhs: z.string(),
rhs: z.string().min(1)
});
const ConditionSchema = z
.object({
operator: z.string(),
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({
name: z.string().trim(),
@@ -59,27 +82,29 @@ export const formSchema = z.object({
permissions: z
.object({
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
conditions: ConditionSchema.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" }
)
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.DynamicSecrets]: DynamicSecretPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
@@ -98,7 +123,7 @@ export const formSchema = z.object({
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
@@ -110,8 +135,22 @@ export const formSchema = z.object({
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 formConditions: z.infer<typeof ConditionSchema>[] = [];
const formConditions: z.infer<typeof ConditionSchema> = [];
Object.entries(caslConditions).forEach(([type, condition]) => {
if (typeof condition === "string") {
formConditions.push({
@@ -138,12 +177,15 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: Partial<TFormSchema["permissions"]> = {};
permissions.forEach((permission) => {
const { subject: caslSub, action, conditions } = permission;
const { subject: caslSub, action, conditions, inverted } = permission;
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
if (
[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.Member,
ProjectPermissionSub.Groups,
ProjectPermissionSub.Identity,
@@ -166,37 +208,67 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.Kms
].includes(subject)
) {
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
const canCreate = action.includes(ProjectPermissionActions.Create);
// from above statement we are sure it won't be undefined
if (subject === ProjectPermissionSub.Secrets) {
if (isConditionalSubjects(subject)) {
if (!formVal[subject]) formVal[subject] = [];
formVal[subject]!.push({
read: canRead,
create: canCreate,
edit: canEdit,
delete: canDelete,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : []
});
if (subject === ProjectPermissionSub.DynamicSecrets) {
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
const canDelete = action.includes(
ProjectPermissionDynamicSecretActions.DeleteRootCredential
);
const canCreate = action.includes(
ProjectPermissionDynamicSecretActions.CreateRootCredential
);
const canLease = action.includes(ProjectPermissionDynamicSecretActions.Lease);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
[ProjectPermissionDynamicSecretActions.ReadRootCredential]: canRead,
[ProjectPermissionDynamicSecretActions.CreateRootCredential]: canCreate,
[ProjectPermissionDynamicSecretActions.EditRootCredential]: canEdit,
[ProjectPermissionDynamicSecretActions.DeleteRootCredential]: canDelete,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted,
[ProjectPermissionDynamicSecretActions.Lease]: canLease
});
} else {
// for other subjects
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
const canCreate = action.includes(ProjectPermissionActions.Create);
formVal[subject]!.push({
read: canRead,
create: canCreate,
edit: canEdit,
delete: canDelete,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
}
} else {
// deduplicate multiple rules for other policies
// because they don't have condition it doesn't make sense for multiple rules
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
const canCreate = action.includes(ProjectPermissionActions.Create);
if (!formVal[subject]) formVal[subject] = [{}];
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = 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 canDelete = action.includes(ProjectPermissionActions.Delete);
if (!formVal[subject]) formVal[subject] = [{}];
// 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;
} else if (subject === ProjectPermissionSub.SecretRollback) {
const canRead = action.includes(ProjectPermissionActions.Read);
@@ -206,12 +278,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = 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) {
const canRead = action.includes(ProjectPermissionCmekActions.Read);
const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
@@ -264,7 +330,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
Object.entries(formVal || {}).forEach(([subject, rules]) => {
rules.forEach((actions) => {
const caslActions = Object.keys(actions).filter(
(el) => actions?.[el as keyof typeof actions] && el !== "conditions"
(el) => actions?.[el as keyof typeof actions] && el !== "conditions" && el !== "inverted"
);
const caslConditions =
"conditions" in actions
@@ -274,6 +340,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
permissions.push({
action: caslActions,
subject,
inverted: (actions as { inverted?: boolean })?.inverted,
conditions: caslConditions
});
});
@@ -288,7 +355,7 @@ export type TProjectPermissionObject = {
label: string;
value: keyof Omit<
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
"conditions"
"conditions" | "inverted"
>;
}[];
};
@@ -306,7 +373,42 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
},
[ProjectPermissionSub.SecretFolders]: {
title: "Secret Folders",
actions: [{ label: "Read Only", value: "read" }]
actions: [
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretImports]: {
title: "Secret Imports",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.DynamicSecrets]: {
title: "Dynamic Secrets",
actions: [
{
label: "Read root credentials",
value: ProjectPermissionDynamicSecretActions.ReadRootCredential
},
{
label: "Create root credentials",
value: ProjectPermissionDynamicSecretActions.CreateRootCredential
},
{
label: "Modify root credentials",
value: ProjectPermissionDynamicSecretActions.EditRootCredential
},
{
label: "Remove root credentials",
value: ProjectPermissionDynamicSecretActions.DeleteRootCredential
},
{ label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease }
]
},
[ProjectPermissionSub.Cmek]: {
title: "KMS",
@@ -332,7 +434,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Workspace]: {
[ProjectPermissionSub.Project]: {
title: "Project",
actions: [
{ label: "Update project details", value: "edit" },

View File

@@ -10,13 +10,15 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
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 { SecretPermissionConditions } from "./components/SecretPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState";
import {
formRolePermission2API,
formSchema,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
rolePermission2Form,
TFormSchema
@@ -27,6 +29,17 @@ type Props = {
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) => {
const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
@@ -130,17 +143,15 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
<div className="py-4">
{!isLoading && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
<GeneralPermissionOptions
<GeneralPermissionPolicies
subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`}
isDisabled={isDisabled}
>
{subject === ProjectPermissionSub.Secrets ? (
<SecretPermissionConditions isDisabled={isDisabled} />
) : undefined}
</GeneralPermissionOptions>
{renderConditionalComponents(subject, isDisabled)}
</GeneralPermissionPolicies>
))}
</div>
</FormProvider>

View File

@@ -0,0 +1,176 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { TFormSchema } from "../ProjectRoleModifySection.utils";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
type Props = {
position?: number;
isDisabled?: boolean;
type:
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports;
};
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-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="mb-2 text-sm text-mineshaft-400">
When this policy should apply (always if no conditions are added).
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${type}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
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="flex w-36 items-center space-x-2">
<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"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</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} />
</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: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};

View File

@@ -1,14 +1,24 @@
import { cloneElement } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
faChevronDown,
faChevronRight,
faInfoCircle,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, Checkbox, Tag } from "@app/components/v2";
import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils";
import {
isConditionalSubjects,
TFormSchema,
TProjectPermissionObject
} from "../ProjectRoleModifySection.utils";
type Props<T extends ProjectPermissionSub> = {
title: string;
@@ -18,7 +28,7 @@ type Props<T extends ProjectPermissionSub> = {
isDisabled?: boolean;
};
export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
subject,
actions,
children,
@@ -63,6 +73,44 @@ export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema
<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="mt-4 mb-6 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"
>
<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">
@@ -98,10 +146,10 @@ export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema
<div
className={twMerge(
"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
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"

View File

@@ -15,6 +15,7 @@ import { ProjectPermissionSub } from "@app/context";
import {
formSchema,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
TFormSchema
} from "../ProjectRoleModifySection.utils";
@@ -89,7 +90,7 @@ export const NewPermissionRule = ({ onClose }: Props) => {
<Button
onClick={form.handleSubmit((el) => {
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
if (rootPolicyValue && selectedSubject === ProjectPermissionSub.Secrets) {
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
rootForm.setValue(
`permissions.${el.type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -0,0 +1,32 @@
import { GlobPermissionInfo } from "@app/components/permissions";
import { SelectItem } from "@app/components/v2";
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
export const getConditionOperatorHelperInfo = (type: PermissionConditionOperators) => {
switch (type) {
case PermissionConditionOperators.$EQ:
return "Value should equal specified value.";
case PermissionConditionOperators.$NEQ:
return "Value should not equal specified value.";
case PermissionConditionOperators.$IN:
return "List of comma-separated values that match a given value.";
case PermissionConditionOperators.$GLOB:
return <GlobPermissionInfo />;
default:
return "";
}
};
export const renderOperatorSelectItems = (type: string) => {
if (type === "secretTags") {
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
}
return (
<>
<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>
</>
);
};

View File

@@ -1,27 +1,34 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle, 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 {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
import { TFormSchema } from "../ProjectRoleModifySection.utils";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
type Props = {
position?: number;
isDisabled?: boolean;
};
const getValueLabel = (type: string) => {
if (type === "environment") return "Environment slug";
if (type === "secretPath") return "Folder path";
return "";
};
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
@@ -30,10 +37,18 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
});
return (
<div className="mt-6 border-t border-t-gray-800 bg-mineshaft-800 pt-2">
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="mb-2 text-sm text-mineshaft-400">
When this policy should apply (always if no conditions are added).
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const lhs = watch(`permissions.secrets.${position}.conditions.${index}.lhs`);
const condition = watch(`permissions.secrets.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
@@ -52,17 +67,25 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
onValueChange={(e) => {
setValue(
`permissions.secrets.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
<SelectItem value="secretName">Secret Name</SelectItem>
<SelectItem value="secretTags">Secret Tags</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="w-36">
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.operator`}
@@ -78,16 +101,22 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
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>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
@@ -99,7 +128,7 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} placeholder={getValueLabel(lhs)} />
<Input {...field} />
</FormControl>
)}
/>
@@ -124,7 +153,6 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
@@ -140,7 +168,7 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
})
}
>
New Condition
Add Condition
</Button>
</div>
</div>

View File

@@ -12,6 +12,7 @@ import { PermissionDeniedBanner } from "@app/components/permissions";
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
@@ -37,7 +38,7 @@ import { ActionBar } from "./components/ActionBar";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { PitDrawer } from "./components/PitDrawer";
import { SecretDropzone } from "./components/SecretDropzone";
import { SecretListView } from "./components/SecretListView";
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
import { SnapshotView } from "./components/SnapshotView";
import {
StoreProvider,
@@ -83,8 +84,24 @@ const SecretMainPageContent = () => {
const secretPath = (router.query.secretPath as string) || "/";
const canReadSecret = permission.can(
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(
ProjectPermissionDynamicSecretActions.ReadRootCredential,
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
);
const canDoReadRollback = permission.can(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback
@@ -93,11 +110,12 @@ const SecretMainPageContent = () => {
const defaultFilterState = {
tags: {},
searchFilter: (router.query.searchFilter as string) || "",
// these should always be on by default for the UI, they will be disabled for the query below based off permissions
include: {
[RowType.Folder]: true,
[RowType.Import]: canReadSecret,
[RowType.DynamicSecret]: canReadSecret,
[RowType.Secret]: canReadSecret
[RowType.Import]: true,
[RowType.DynamicSecret]: true,
[RowType.Secret]: true
}
};
@@ -105,19 +123,6 @@ const SecretMainPageContent = () => {
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
// change filters if permissions change at different paths/env
useEffect(() => {
setFilter((prev) => ({
...prev,
include: {
[RowType.Folder]: true,
[RowType.Import]: canReadSecret,
[RowType.DynamicSecret]: canReadSecret,
[RowType.Secret]: canReadSecret
}
}));
}, [canReadSecret]);
useEffect(() => {
if (
!isWorkspaceLoading &&
@@ -145,9 +150,9 @@ const SecretMainPageContent = () => {
orderBy,
search: debouncedSearchFilter,
orderDirection,
includeImports: canReadSecret && filter.include.import,
includeImports: canReadSecretImports && filter.include.import,
includeFolders: filter.include.folder,
includeDynamicSecrets: canReadSecret && filter.include.dynamic,
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
includeSecrets: canReadSecret && filter.include.secret,
tags: filter.tags
});
@@ -210,8 +215,20 @@ const SecretMainPageContent = () => {
isPaused: !canDoReadRollback
});
const noAccessSecretCount = Math.max(
(page * perPage > totalCount ? totalCount % perPage : perPage) -
(imports?.length || 0) -
(folders?.length || 0) -
(secrets?.length || 0) -
(dynamicSecrets?.length || 0),
0
);
const isNotEmpty = Boolean(
secrets?.length || folders?.length || imports?.length || dynamicSecrets?.length
secrets?.length ||
folders?.length ||
imports?.length ||
dynamicSecrets?.length ||
noAccessSecretCount
);
const handleSortToggle = () =>
@@ -330,7 +347,6 @@ const SecretMainPageContent = () => {
setFilter(defaultFilterState);
setDebouncedSearchFilter("");
};
return (
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<SecretV2MigrationSection />
@@ -411,49 +427,53 @@ const SecretMainPageContent = () => {
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{canReadSecret && imports?.length && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
secretImports={imports}
isFetching={isDetailsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
importedSecrets={importedSecrets}
/>
)}
{folders?.length && (
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadSecret && dynamicSecrets?.length && (
<DynamicSecretListView
environment={environment}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecrets={dynamicSecrets}
/>
)}
{canReadSecret && secrets?.length && (
<SecretListView
secrets={secrets}
tags={tags}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
)}
{canReadSecretImports && Boolean(imports?.length) && (
<SecretImportListView
searchTerm={debouncedSearchFilter}
secretImports={imports}
isFetching={isDetailsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
importedSecrets={importedSecrets}
/>
)}
{Boolean(folders?.length) && (
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
onNavigateToFolder={handleResetFilter}
/>
)}
{canReadDynamicSecret && Boolean(dynamicSecrets?.length) && (
<DynamicSecretListView
environment={environment}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecrets={dynamicSecrets}
/>
)}
{canReadSecret && Boolean(secrets?.length) && (
<SecretListView
secrets={secrets}
tags={tags}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
/>
)}
{canReadSecret && <SecretNoAccessListView count={noAccessSecretCount} />}
{!canReadSecret &&
!canReadDynamicSecret &&
!canReadSecretImports &&
folders?.length === 0 && <PermissionDeniedBanner />}
</div>
</div>
</div>
{!isDetailsLoading && totalCount > 0 && (
<Pagination
startAdornment={

View File

@@ -47,8 +47,8 @@ import {
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription
} from "@app/context";
import { usePopUp } from "@app/hooks";
@@ -123,12 +123,6 @@ export const ActionBar = ({
const { reset: resetSelectedSecret } = useSelectedSecretActions();
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) => {
try {
await createFolder({
@@ -436,7 +430,12 @@ export const ActionBar = ({
<div className="flex items-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
@@ -467,12 +466,7 @@ export const ActionBar = ({
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
>
{(isAllowed) => (
<Button
@@ -491,8 +485,13 @@ export const ActionBar = ({
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={ProjectPermissionDynamicSecretActions.CreateRootCredential}
a={subject(ProjectPermissionSub.DynamicSecrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
@@ -516,7 +515,10 @@ export const ActionBar = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.SecretImports, {
environment,
secretPath
})}
>
{(isAllowed) => (
<Button
@@ -556,7 +558,12 @@ export const ActionBar = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
renderTooltip
allowedLabel="Move"
>
@@ -575,7 +582,12 @@ export const ActionBar = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
renderTooltip
allowedLabel="Delete"
>

View File

@@ -26,7 +26,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
@@ -60,7 +60,6 @@ export const DynamicSecretLease = ({
path: secretPath,
dynamicSecretName
});
const deleteDynamicSecretLease = useRevokeDynamicSecretLease();
@@ -140,8 +139,8 @@ export const DynamicSecretLease = ({
<Td>
<div className="flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={ProjectPermissionDynamicSecretActions.Lease}
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip
allowedLabel="Renew"
>
@@ -159,8 +158,8 @@ export const DynamicSecretLease = ({
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={ProjectPermissionDynamicSecretActions.Lease}
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>
@@ -179,8 +178,11 @@ export const DynamicSecretLease = ({
</ProjectPermissionCan>
{status === DynamicSecretLeaseStatus.FailedDeletion && (
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={ProjectPermissionDynamicSecretActions.Lease}
a={subject(ProjectPermissionSub.DynamicSecrets, {
environment,
secretPath
})}
renderTooltip
allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems."
>
@@ -209,9 +211,19 @@ export const DynamicSecretLease = ({
</TableContainer>
{!isLeaseLoading && Boolean(leases?.length) && (
<div className="mt-6 flex items-center space-x-4">
<Button onClick={onClickNewLease} size="xs">
New Lease
</Button>
<ProjectPermissionCan
I={ProjectPermissionDynamicSecretActions.Lease}
a={subject(ProjectPermissionSub.DynamicSecrets, {
environment,
secretPath
})}
>
{(isAllowed) => (
<Button onClick={onClickNewLease} size="xs" isDisabled={!isAllowed}>
New Lease
</Button>
)}
</ProjectPermissionCan>
<Button onClick={onClose} variant="plain" colorSchema="secondary" size="xs">
Close
</Button>

View File

@@ -18,7 +18,7 @@ import {
Tag,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteDynamicSecret } from "@app/hooks/api";
import {
@@ -132,17 +132,27 @@ export const DynamicSecretListView = ({
)}
</div>
<div className="flex items-center space-x-2 px-4 py-2">
<Button
size="xs"
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
isDisabled={isRevoking}
onClick={(evt) => {
evt.stopPropagation();
handlePopUpOpen("createDynamicSecretLease", secret);
}}
<ProjectPermissionCan
I={ProjectPermissionDynamicSecretActions.Lease}
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip
allowedLabel="Edit"
>
Generate
</Button>
{(isAllowed) => (
<Button
size="xs"
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
isDisabled={isRevoking || !isAllowed}
onClick={(evt) => {
evt.stopPropagation();
handlePopUpOpen("createDynamicSecretLease", secret);
}}
>
Generate
</Button>
)}
</ProjectPermissionCan>
{secret.status === DynamicSecretStatus.FailedDeletion && (
<Tooltip content="This action will remove the secret from internal storage, but it will remain in external systems. Use this option only after you've confirmed that your external leases are handled.">
<Button
@@ -165,8 +175,8 @@ export const DynamicSecretListView = ({
</div>
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={ProjectPermissionDynamicSecretActions.EditRootCredential}
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip
allowedLabel="Edit"
>
@@ -187,8 +197,8 @@ export const DynamicSecretListView = ({
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={ProjectPermissionDynamicSecretActions.DeleteRootCredential}
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>

View File

@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
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 { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -33,11 +33,6 @@ export const FolderListView = ({
"deleteFolder"
] as const);
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: 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">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
renderTooltip
allowedLabel="Edit"
>
@@ -150,12 +140,7 @@ export const FolderListView = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>

View File

@@ -142,7 +142,12 @@ export const CopySecretsFromBoard = ({
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button

View File

@@ -250,7 +250,12 @@ export const SecretDropzone = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<input
@@ -287,7 +292,12 @@ export const SecretDropzone = ({
{!isSmaller && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button

View File

@@ -67,7 +67,7 @@ export const SecretImportItem = ({
isReplicationExpand,
importedSecrets = [],
searchTerm = "",
secretPath,
secretPath = "/",
environment,
secretImport,
onExpandReplicateSecrets: onExpandReplicate
@@ -209,7 +209,7 @@ export const SecretImportItem = ({
{isReplication && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
renderTooltip
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">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.SecretImports, {
environment,
secretPath: secretPath || "/"
})}
renderTooltip
allowedLabel="Change order"
>
@@ -256,7 +259,7 @@ export const SecretImportItem = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>

View File

@@ -84,26 +84,41 @@ export const SecretDetailSidebar = ({
resolver: zodResolver(formSchema),
values: secret
});
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({
control,
name: "tags"
});
const secretKey = secret?.key || "";
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
(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: secretKey,
secretTags: selectTagSlugs
})
);
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
) && cannotEditSecret;
const overrideAction = watch("overrideAction");
const isOverridden =
@@ -194,7 +209,12 @@ export const SecretDetailSidebar = ({
</FormControl>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Controller
@@ -221,7 +241,12 @@ export const SecretDetailSidebar = ({
<div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Switch
@@ -277,7 +302,12 @@ export const SecretDetailSidebar = ({
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
@@ -367,6 +397,7 @@ export const SecretDetailSidebar = ({
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()}
isDisabled={cannotEditSecret}
>
Create Reminder
</Button>
@@ -388,7 +419,12 @@ export const SecretDetailSidebar = ({
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Switch
@@ -450,7 +486,12 @@ export const SecretDetailSidebar = ({
<div className="flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Button
@@ -465,7 +506,12 @@ export const SecretDetailSidebar = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>

View File

@@ -81,15 +81,6 @@ export const SecretItem = memo(
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const {
handleSubmit,
@@ -107,6 +98,8 @@ export const SecretItem = memo(
resolver: zodResolver(formSchema)
});
const secretName = watch("key");
const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote");
@@ -118,11 +111,33 @@ export const SecretItem = memo(
(prev, curr) => ({ ...prev, [curr.id]: true }),
{}
);
const selectedTagSlugs = selectedTags.map((i) => i.slug);
const { fields, append, remove } = useFieldArray({
control,
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 [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
useEffect(() => {
@@ -309,7 +324,12 @@ export const SecretItem = memo(
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild disabled={!isAllowed}>
@@ -384,7 +404,12 @@ export const SecretItem = memo(
</DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
renderTooltip
allowedLabel="Override"
>
@@ -440,7 +465,12 @@ export const SecretItem = memo(
<Popover>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
>
{(isAllowed) => (
<PopoverTrigger asChild disabled={!isAllowed}>
@@ -519,7 +549,12 @@ export const SecretItem = memo(
</Tooltip>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
renderTooltip
allowedLabel="Delete"
>

View File

@@ -16,7 +16,6 @@ import { WsTag } from "@app/hooks/api/types";
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
import { Filter } from "../../SecretMainPage.types";
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
import { SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
@@ -31,16 +30,6 @@ type Props = {
isProtectedBranch?: boolean;
};
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
secrets.filter(({ key, value, tags }) => {
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
const searchTerm = filter.searchFilter.toLowerCase();
return (
(!isTagFilterActive || tags?.some(({ id }) => filter.tags?.[id])) &&
(key.toLowerCase().includes(searchTerm) || value?.toLowerCase().includes(searchTerm))
);
});
export const SecretListView = ({
secrets = [],
environment,

View File

@@ -10,6 +10,7 @@ import {
faCopy,
faEllipsis,
faKey,
faLock,
faShare,
faTags
} from "@fortawesome/free-solid-svg-icons";
@@ -71,7 +72,8 @@ export enum FontAwesomeSpriteName {
Close = "close",
CheckedCircle = "check-circle",
ReplicatedSecretKey = "secret-replicated",
ShareSecret = "share-secret"
ShareSecret = "share-secret",
KeyLock = "key-lock"
}
// this is an optimization technique
@@ -88,5 +90,6 @@ export const FontAwesomeSpriteSymbols = [
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret }
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock }
];

View File

@@ -0,0 +1,49 @@
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
import { FontAwesomeSpriteName } from "./SecretListView.utils";
type Props = {
count: number;
};
export const SecretNoAccessListView = ({ count }: Props) => {
return (
<>
{Array.from(Array(count)).map((_, i) => (
<Tooltip
className="max-w-sm"
asChild
content="You do not have permission to view this secret"
key={`no-access-secret-${i + 1}`}
>
<div className="flex border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700">
<div className="flex h-11 w-11 items-center justify-center px-4 py-3">
<FontAwesomeSymbol
className="ml-3 block h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.KeyLock}
/>
</div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
<Input
autoComplete="off"
isReadOnly
variant="plain"
value="NO ACCESS"
isDisabled
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/>
</div>
<div
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</div>
</Tooltip>
))}
</>
);
};

View File

@@ -1 +1,2 @@
export { SecretListView } from "./SecretListView";
export { SecretNoAccessListView } from "./SecretNoAccessListView";

View File

@@ -72,7 +72,10 @@ import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
import { SecretOverviewDynamicSecretRow } from "@app/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow";
import { SecretOverviewTableRow } from "@app/views/SecretOverviewPage/components/SecretOverviewTableRow";
import {
SecretNoAccessOverviewTableRow,
SecretOverviewTableRow
} from "@app/views/SecretOverviewPage/components/SecretOverviewTableRow";
import { SecretTableResourceCount } from "@app/views/SecretOverviewPage/components/SecretTableResourceCount";
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
@@ -210,7 +213,10 @@ export const SecretOverviewPage = () => {
totalFolderCount,
totalSecretCount,
totalDynamicSecretCount,
totalCount = 0
totalCount = 0,
totalUniqueFoldersInPage,
totalUniqueSecretsInPage,
totalUniqueDynamicSecretsInPage
} = overview ?? {};
useResetPageHelper({
@@ -275,7 +281,7 @@ export const SecretOverviewPage = () => {
if (
permission.can(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
subject(ProjectPermissionSub.SecretFolders, { environment: env.slug, secretPath })
)
) {
const folder = getFolderByNameAndEnv(oldFolderName, env.slug);
@@ -478,20 +484,13 @@ export const SecretOverviewPage = () => {
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
const canCreateFolder = permission.rules.some((rule) =>
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
)
? permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: slug,
secretPath: parentPath
})
)
: permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: parentPath })
);
const canCreateFolder = permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: slug,
secretPath: parentPath
})
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
projectId: workspaceId,
@@ -822,7 +821,7 @@ export const SecretOverviewPage = () => {
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
a={ProjectPermissionSub.SecretFolders}
>
{(isAllowed) => (
<Button
@@ -1047,6 +1046,16 @@ export const SecretOverviewPage = () => {
scrollOffset={debouncedScrollOffset}
/>
))}
<SecretNoAccessOverviewTableRow
environments={visibleEnvs}
count={Math.max(
(page * perPage > totalCount ? totalCount % perPage : perPage) -
(totalUniqueFoldersInPage || 0) -
(totalUniqueDynamicSecretsInPage || 0) -
(totalUniqueSecretsInPage || 0),
0
)}
/>
</>
)}
</TBody>

View File

@@ -93,23 +93,14 @@ export const CreateSecretForm = ({
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
const canCreateFolder = permission.rules.some((rule) =>
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
)
? permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: env.slug,
secretPath: parentPath
})
)
: permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath: parentPath
})
);
const canCreateFolder = permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: env.slug,
secretPath: parentPath
})
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
projectId: workspaceId,
@@ -250,7 +241,9 @@ export const CreateSecretForm = ({
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: environmentSlug.slug,
secretPath
secretPath,
secretName: "*",
secretTags: ["*"]
})
)
)

View File

@@ -1,4 +1,4 @@
import { useCallback,useState } from "react";
import { useCallback, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
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 { 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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
@@ -63,8 +63,8 @@ export const SecretEditRow = ({
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const toggleModal = useCallback(() => {
setIsModalOpen((prev) => !prev)
}, [])
setIsModalOpen((prev) => !prev);
}, []);
const handleFormReset = () => {
reset();
@@ -114,7 +114,6 @@ export const SecretEditRow = ({
return (
<div className="group flex w-full cursor-text items-center space-x-2">
<DeleteActionModal
isOpen={isModalOpen}
onClose={toggleModal}
@@ -151,8 +150,13 @@ export const SecretEditRow = ({
{isDirty ? (
<>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
I={isCreatable ? ProjectPermissionActions.Create : ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: ["*"]
})}
>
{(isAllowed) => (
<div>
@@ -201,7 +205,12 @@ export const SecretEditRow = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: ["*"]
})}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">

View File

@@ -0,0 +1,51 @@
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Td, Tooltip, Tr } from "@app/components/v2";
type Props = {
environments: { name: string; slug: string }[];
count: number;
};
export const SecretNoAccessOverviewTableRow = ({ environments = [], count }: Props) => {
return (
<>
{Array.from(Array(count)).map((_, j) => (
<Tr key={`no-access-secret-overview-${j + 1}`} isHoverable isSelectable className="group">
<Td className="sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding py-0 px-0 group-hover:bg-mineshaft-700">
<div className="h-full w-full border-r border-mineshaft-600 py-2.5 px-5">
<Tooltip
asChild
content="You do not have permission to view this secret"
className="max-w-sm"
>
<div className="flex items-center space-x-5">
<div className="text-bunker-300">
<FontAwesomeIcon className="block" icon={faLock} />
</div>
<div className="blur-sm">NO ACCESS</div>
</div>
</Tooltip>
</div>
</Td>
{environments.map(({ slug }, i) => {
return (
<Td
key={`sec-overview-${slug}-${i + 1}-value`}
className="py-0 px-0 group-hover:bg-mineshaft-700"
>
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
<div className="flex justify-center">
<FontAwesomeIcon icon={faCircle} />
</div>
</div>
</Td>
);
})}
</Tr>
))}
</>
);
};

View File

@@ -18,7 +18,7 @@ import {
} from "@app/context";
import { useToggle } from "@app/hooks";
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";
type Props = {
@@ -42,15 +42,16 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
const isReadOnly = environments.some((env) => {
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 =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
permission.can(ProjectPermissionActions.Read, secretPermissionSubject) &&
permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject);
if (isSecretInEnvReadOnly) {
return true;
}

View File

@@ -1 +1,2 @@
export { SecretNoAccessOverviewTableRow } from "./SecretNoAccessOverviewTableRow";
export { SecretOverviewTableRow } from "./SecretOverviewTableRow";

View File

@@ -57,7 +57,12 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
const shouldShowDelete = userAvailableEnvs.some((env) =>
permission.can(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath,
secretName: "*",
secretTags: ["*"]
})
)
);
@@ -76,34 +81,43 @@ export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntri
const promises = userAvailableEnvs.map(async (env) => {
// additional check: ensure that bulk delete is only executed on envs that user has access to
if (
permission.cannot(
permission.can(
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 (folderRecord) => {
const folder = folderRecord[env.slug];
if (folder) {
processedEntries += 1;
await deleteFolder({
folderId: folder?.id,
path: secretPath,
environment: env.slug,
projectId: workspaceId
});
}
})
);
}
await Promise.all(
Object.values(selectedEntries.folder).map(async (folderRecord) => {
const folder = folderRecord[env.slug];
if (folder) {
processedEntries += 1;
await deleteFolder({
folderId: folder?.id,
path: secretPath,
environment: env.slug,
projectId: workspaceId
});
}
})
);
const secretsToDelete = Object.values(selectedEntries.secret).reduce(
const secretsToDelete = Object.keys(selectedEntries.secret).reduce(
(accum: TDeleteSecretBatchDTO["secrets"], secretRecord) => {
const entry = secretRecord[env.slug];
if (entry) {
const canDeleteSecret = permission.can(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath,
secretName: entry.key,
secretTags: (entry?.tags || []).map((i) => i.slug)
})
);
if (entry && canDeleteSecret) {
return [
...accum,
{

View File

@@ -136,10 +136,7 @@ export const DeleteProjectSection = () => {
<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>
<div className="space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Workspace}
>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
isLoading={isDeleting}

View File

@@ -318,10 +318,7 @@ export const EncryptionTab = () => {
/>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Workspace}
>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
colorSchema="secondary"

View File

@@ -22,7 +22,6 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>;
export const ProjectNameChangeSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useRenameWorkspace();
@@ -83,7 +82,7 @@ export const ProjectNameChangeSection = () => {
</div>
</div>
<div className="max-w-md">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Controller
defaultValue=""
@@ -103,7 +102,7 @@ export const ProjectNameChangeSection = () => {
)}
</ProjectPermissionCan>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
{(isAllowed) => (
<Button
colorSchema="secondary"