mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 22:26:07 +00:00
feat: updated frontend project permission logic
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
};
|
||||||
+86
-40
@@ -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" },
|
||||||
|
|||||||
+17
-6
@@ -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>
|
||||||
|
|||||||
+155
@@ -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
-4
@@ -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"
|
||||||
+2
-1
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
+6
-4
@@ -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."
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
+6
-1
@@ -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
|
||||||
|
|||||||
+7
-4
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
+57
-15
@@ -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
|
||||||
|
|||||||
+11
-18
@@ -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: ["*"]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+11
-7
@@ -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>
|
||||||
|
|||||||
+10
-9
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-24
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-4
@@ -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}
|
||||||
|
|||||||
+1
-4
@@ -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"
|
||||||
|
|||||||
+2
-3
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user