feat(rbac): added new permission check for workspace in frontend

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent 549121f44e
commit 6671699867
48 changed files with 3446 additions and 2804 deletions
@@ -4,7 +4,7 @@ import { IUser, Key, Membership, MembershipOrg, User } from "../../models";
import { EventType } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, MEMBER } from "../../variables";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
@@ -15,6 +15,8 @@ import {
getUserProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import Role from "../../models/role";
import { BadRequestError } from "../../utils/errors";
/**
* Check that user is a member of workspace with id [workspaceId]
@@ -107,10 +109,6 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
params: { membershipId }
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
if (![ADMIN, MEMBER].includes(role)) {
throw new Error("Failed to validate role");
}
// validate target membership
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
"user"
@@ -129,9 +127,32 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
ProjectPermissionSub.Member
);
const oldRole = membershipToChangeRole.role;
membershipToChangeRole.role = role;
await membershipToChangeRole.save();
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
const wsRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: membershipToChangeRole.workspace
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
const membership = await Membership.findByIdAndUpdate(
membershipId,
{
role
},
{
new: true
}
);
await EEAuditLogService.createAuditLog(
req.authData,
@@ -140,8 +161,8 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole,
newRole: membershipToChangeRole.role
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
@@ -150,7 +171,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
);
return res.status(200).send({
membership: membershipToChangeRole
membership
});
};
@@ -25,7 +25,6 @@ import {
userHasWriteOnlyAbility
} from "../../ee/helpers/checkMembershipPermissions";
import _ from "lodash";
import { BatchSecret } from "../../types/secret";
import {
getFolderByPath,
getFolderIdFromServiceToken,
@@ -56,8 +55,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
body: { secretPath, folderId }
} = validatedData;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const createSecrets: any[] = [];
const updateSecrets: any[] = [];
const deleteSecrets: { _id: Types.ObjectId; secretName: string }[] = [];
const actions: IAction[] = [];
@@ -111,7 +110,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId),
workspace: workspaceId,
folder: folderId,
secretBlindIndex,
algorithm: ALGORITHM_AES_256_GCM,
@@ -126,7 +125,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id),
_id: request.secret._id,
secretBlindIndex,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
@@ -145,7 +144,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
// handle create secrets
let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets);
createdSecrets = (await Secret.insertMany(createSecrets)) as any;
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => {
+18 -17
View File
@@ -1,7 +1,4 @@
import {
IUser,
User,
} from "../models";
import { IUser, User } from "../models";
import { sendMail } from "./nodemailer";
/**
@@ -12,7 +9,7 @@ import { sendMail } from "./nodemailer";
*/
export const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email,
email
}).save();
return user;
@@ -49,11 +46,11 @@ export const completeAccount = async ({
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
verifier
}: {
userId: string;
firstName: string;
lastName: string;
lastName?: string;
encryptionVersion: number;
protectedKey: string;
protectedKeyIV: string;
@@ -66,7 +63,7 @@ export const completeAccount = async ({
verifier: string;
}) => {
const options = {
new: true,
new: true
};
const user = await User.findByIdAndUpdate(
userId,
@@ -82,7 +79,7 @@ export const completeAccount = async ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
verifier
},
options
);
@@ -100,22 +97,26 @@ export const completeAccount = async ({
export const checkUserDevice = async ({
user,
ip,
userAgent,
userAgent
}: {
user: IUser;
ip: string;
userAgent: string;
}) => {
const isDeviceSeen = user.devices.some((device) => device.ip === ip && device.userAgent === userAgent);
const isDeviceSeen = user.devices.some(
(device) => device.ip === ip && device.userAgent === userAgent
);
if (!isDeviceSeen) {
// case: unseen login ip detected for user
// -> notify user about the sign-in from new ip
user.devices = user.devices.concat([{
user.devices = user.devices.concat([
{
ip: String(ip),
userAgent,
}]);
userAgent
}
]);
await user.save();
@@ -128,8 +129,8 @@ export const checkUserDevice = async ({
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent,
},
userAgent
}
});
}
}
};
+1 -8
View File
@@ -1,7 +1,6 @@
import express from "express";
const router = express.Router();
import { body, param } from "express-validator";
import { requireAuth, validateRequest } from "../../middleware";
import { requireAuth } from "../../middleware";
import { membershipController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
@@ -14,8 +13,6 @@ router.get(
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
param("workspaceId").exists().trim(),
validateRequest,
membershipController.validateMembership
);
@@ -25,8 +22,6 @@ router.delete(
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
param("membershipId").exists().trim(),
validateRequest,
membershipController.deleteMembership
);
@@ -36,8 +31,6 @@ router.post(
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
body("role").exists().trim(),
validateRequest,
membershipController.changeMembershipRole
);
+4 -4
View File
@@ -34,7 +34,7 @@ export enum ProjectPermissionSub {
Folders = "folders"
}
type GenericFields = {
type SubjectFields = {
environment: string;
secretPath?: string;
};
@@ -42,17 +42,17 @@ type GenericFields = {
export type ProjectPermissionSet =
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & GenericFields)
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & GenericFields)
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & GenericFields)
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
+8 -2
View File
@@ -28,7 +28,13 @@ export interface BatchSecretRequest {
}
export interface BatchSecret {
_id: string;
version?: number;
_id?: string;
user?: string;
environment: string;
workspace?: string;
algorithm?: string;
keyEncoding?: string;
type: "shared" | "personal";
secretName: string;
secretBlindIndex: string;
@@ -42,5 +48,5 @@ export interface BatchSecret {
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
folder: string
folder: string;
}
+1 -1
View File
@@ -117,7 +117,7 @@ export const CompletedAccountSignupV3 = z.object({
body: z.object({
email: z.string().email().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional().nullish(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
+6 -4
View File
@@ -115,8 +115,8 @@ export const GetSecretVersionsV1 = z.object({
secretId: z.string().trim()
}),
query: z.object({
offset: z.number(),
limit: z.number()
offset: z.coerce.number(),
limit: z.coerce.number()
})
});
@@ -175,17 +175,19 @@ const batchUpdateRequestV2 = z.object({
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
tags: z.object({
tags: z
.object({
_id: z.string().trim(),
name: z.string().trim(),
slug: z.string().trim()
})
.array()
});
export const BatchSecretsV2 = z.object({
body: z.object({
workspaceId: z.string().trim(),
folderId: z.string().trim(),
folderId: z.string().trim().default("root"),
environment: z.string().trim(),
secretPath: z.string().trim().optional(),
requests: z
+6 -6
View File
@@ -130,8 +130,8 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
query: z.object({
environment: z.string().trim(),
folderId: z.string().trim().default("root"),
offset: z.number(),
limit: z.number()
offset: z.coerce.number(),
limit: z.coerce.number()
})
});
@@ -161,8 +161,8 @@ export const GetWorkspaceLogsV1 = z.object({
workspaceId: z.string().trim()
}),
query: z.object({
offset: z.number(),
limit: z.number(),
offset: z.coerce.number(),
limit: z.coerce.number(),
sortBy: z.string().trim().optional(),
userId: z.string().trim().optional(),
actionNames: z.string().trim().optional()
@@ -178,8 +178,8 @@ export const GetWorkspaceAuditLogsV1 = z.object({
userAgentType: z.nativeEnum(UserAgentType).nullable().optional(),
startDate: z.string().datetime().nullable().optional(),
endDate: z.string().datetime().nullable().optional(),
offset: z.number(),
limit: z.number(),
offset: z.coerce.number(),
limit: z.coerce.number(),
actor: z.string().nullish().optional()
})
});
@@ -16,6 +16,15 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
const workspaceId = currentWorkspace?._id || "";
const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId });
console.log(workspaceId);
if (!permission && currentWorkspace) {
return (
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
Failed to load user permissions
</div>
);
}
if (isLoading && workspaceId) {
return (
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
@@ -29,20 +38,8 @@ export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => {
);
}
if (!permission && currentWorkspace) {
return (
<div className="flex items-center justify-center w-screen h-screen bg-bunker-800">
Failed to load user permissions
</div>
);
}
if (!permission) {
return <>children</>;
}
return (
<ProjectPermissionContext.Provider value={permission}>
<ProjectPermissionContext.Provider value={permission!}>
{children}
</ProjectPermissionContext.Provider>
);
@@ -1,3 +1,3 @@
export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext";
export type { ProjectPermissionSet, TProjectPermission } from "./types";
export { ProjectGeneralPermissionActions, ProjectPermissionSubjects } from "./types";
export { ProjectPermissionActions,ProjectPermissionSub } from "./types";
@@ -1,13 +1,13 @@
import { MongoAbility } from "@casl/ability";
import { ForcedSubject, MongoAbility } from "@casl/ability";
export enum ProjectGeneralPermissionActions {
export enum ProjectPermissionActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionSubjects {
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
Settings = "settings",
@@ -21,24 +21,44 @@ export enum ProjectPermissionSubjects {
Workspace = "workspace",
Secrets = "secrets",
SecretImports = "secret-imports",
SecretRollback = "secret-rollback",
Folders = "folders"
}
type SubjectFields = {
environment: string;
secretPath?: string;
};
export type ProjectPermissionSet =
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Secrets]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Folders]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.SecretImports]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Role]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Tags]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Member]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Integrations]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Webhooks]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.AuditLogs]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Environments]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.IpAllowList]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.Settings]
| [ProjectGeneralPermissionActions, ProjectPermissionSubjects.ServiceTokens]
| [ProjectGeneralPermissionActions.Delete, ProjectPermissionSubjects.Workspace]
| [ProjectGeneralPermissionActions.Edit, ProjectPermissionSubjects.Workspace];
| [
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
ProjectPermissionSub.Folders | (ForcedSubject<ProjectPermissionSub.Folders> & SubjectFields)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretImports
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
+3 -2
View File
@@ -7,10 +7,11 @@ export {
OrgPermissionSubjects,
useOrgPermission
} from "./OrgPermissionContext";
export type { TProjectPermission } from "./ProjectPermissionContext";
export {
ProjectGeneralPermissionActions,
ProjectPermissionActions,
ProjectPermissionProvider,
ProjectPermissionSubjects,
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
+1
View File
@@ -1 +1,2 @@
export { withPermission } from "./withPermission";
export { withProjectPermission } from "./withProjectPermission";
@@ -0,0 +1 @@
export { withProjectPermission } from "./withProjectPermission";
@@ -0,0 +1,62 @@
import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } 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";
type Props<T extends Abilities> = (T extends AbilityTuple
? {
action: T[0];
subject: Extract<T[1], SubjectType>;
}
: {
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
export const withProjectPermission = <T extends {}, J extends TProjectPermission>(
Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
) => {
const HOC = (hocProps: 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)) {
return (
<div
className={twMerge(
"container h-full mx-auto flex justify-center items-center",
containerClassName
)}
>
<div
className={twMerge(
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="text-4xl font-medium mb-2">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
</div>
);
}
return <Component {...hocProps} />;
};
HOC.displayName = "WithProjectPermission";
return HOC;
};
+17 -10
View File
@@ -6,8 +6,9 @@ import { useRouter } from "next/router";
import Button from "@app/components/basic/buttons/Button";
import EventFilter from "@app/components/basic/EventFilter";
import { UpgradePlanModal } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks/usePopUp";
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
@@ -23,10 +24,10 @@ interface LogData {
};
serviceAccount?: {
string: string;
},
};
serviceTokenData?: {
name: string;
}
};
actions: {
_id: string;
name: string;
@@ -60,7 +61,8 @@ interface LogDataPoint {
/**
* This is the tab that includes all of the user activity logs
*/
export default function Activity() {
const Activity = withProjectPermission(
() => {
const router = useRouter();
const [eventChosen, setEventChosen] = useState("");
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
@@ -70,9 +72,7 @@ export default function Activity() {
const [currentSidebarAction, toggleSidebar] = useState<string>();
const { t } = useTranslation();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"upgradePlan"
] as const);
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["upgradePlan"] as const);
// this use effect updates the data in case of a new filter being added
useEffect(() => {
@@ -186,12 +186,19 @@ export default function Activity() {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
text={
subscription.slug === null
? "You can see more logs under an Enterprise license"
: "You can see more logs if you switch to Infisical's Business/Professional Plan."
}
/>
)}
</div>
);
}
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
);
Activity.requireAuth = true;
Object.assign(Activity, { requireAuth: true });
export default Activity;
@@ -39,6 +39,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
@@ -55,7 +56,8 @@ import {
UpgradePlanModal
} from "@app/components/v2";
import { leaveConfirmDefaultMessage } from "@app/const";
import { useOrganization, useSubscription, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub,useOrganization, useSubscription, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import {
useBatchSecretsOp,
@@ -123,7 +125,8 @@ type TDeleteSecretImport = { environment: string; secretPath: string };
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
* They will get it back
*/
export const DashboardPage = () => {
export const DashboardPage = withProjectPermission(
() => {
const { subscription } = useSubscription();
const { t } = useTranslation();
const router = useRouter();
@@ -149,7 +152,7 @@ export const DashboardPage = () => {
const [snapshotId, setSnaphotId] = useState<string | null>(null);
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const deletedSecretIds = useRef<{ id: string; secretName: string; }[]>([]);
const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
const folderId = router.query.folderId as string;
@@ -297,7 +300,6 @@ export const DashboardPage = () => {
resolver: yupResolver(schema)
});
const {
register,
control,
@@ -311,7 +313,8 @@ export const DashboardPage = () => {
const isReadOnly = selectedEnv?.isWriteDenied;
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
const canDoRollback = !isReadOnly && !isAddOnly;
const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting;
const isSubmitDisabled =
isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting;
useEffect(() => {
if (!isSnapshotChanging && Boolean(snapshotId)) {
@@ -499,27 +502,31 @@ export const DashboardPage = () => {
// record all deleted ids
// This will make final deletion easier
const onSecretDelete = useCallback((index: number, secretName: string, id?: string, overrideId?: string) => {
if (id) deletedSecretIds.current.push({
const onSecretDelete = useCallback(
(index: number, secretName: string, id?: string, overrideId?: string) => {
if (id)
deletedSecretIds.current.push({
id,
secretName
});
if (overrideId) deletedSecretIds.current.push({
if (overrideId)
deletedSecretIds.current.push({
id: overrideId,
secretName
});
remove(index);
// just the case if this is called from drawer
handlePopUpClose("secretDetails");
}, []);
},
[]
);
const onCreateWsTag = useCallback(
async (tagName: string, tagColor: string) => {
async (tagName: string) => {
try {
await createWsTag({
workspaceID: workspaceId,
tagName,
tagColor,
tagSlug: tagName.replace(" ", "_")
});
handlePopUpClose("addTag");
@@ -753,7 +760,9 @@ export const DashboardPage = () => {
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""}
currentEnv={
userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""
}
isFolderMode
folders={folderData?.dir}
isProjectRelated
@@ -816,9 +825,15 @@ export const DashboardPage = () => {
</IconButton>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SecretRollback}
>
{(isAllowed) => (
<div className="block xl:hidden">
<Tooltip content="Point-in-time Recovery">
<IconButton
isDisabled={!isAllowed}
ariaLabel="recovery"
variant="outline_bg"
onClick={() => handlePopUpOpen("secretSnapshots")}
@@ -827,6 +842,13 @@ export const DashboardPage = () => {
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SecretRollback}
>
{(isAllowed) => (
<div className="hidden xl:block">
<Button
variant="outline_bg"
@@ -840,16 +862,24 @@ export const DashboardPage = () => {
}}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
isLoading={isLoadingSnapshotCount}
isDisabled={!canDoRollback}
isDisabled={!canDoRollback || !isAllowed}
className="h-10"
>
{snapshotCount} Commits
</Button>
</div>
)}
</ProjectPermissionCan>
{!isReadOnly && !isRollbackMode && (
<div className="flex flex-row items-center justify-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<button
type="button"
disabled={!isAllowed}
onClick={() => {
if (!(isReadOnly || isRollbackMode)) {
if (secretContainer.current) {
@@ -867,6 +897,8 @@ export const DashboardPage = () => {
<FontAwesomeIcon icon={faPlus} className="px-2" />
Add Secret
</button>
)}
</ProjectPermissionCan>
<DropdownMenu>
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
<div className="cursor-pointer rounded-r-md border border-mineshaft-500 bg-mineshaft-600 p-2 text-sm text-mineshaft-300 duration-200 hover:border-primary/40 hover:bg-primary/[0.1]">
@@ -876,28 +908,42 @@ export const DashboardPage = () => {
<DropdownMenuContent align="end" className="left-20 z-[60] mt-1 w-[10.8rem]">
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<div className="w-full pb-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Folders}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => handlePopUpOpen("folderForm")}
isDisabled={isReadOnly || isRollbackMode}
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
</div>
<div className="w-full">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretImports}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
onClick={() => handlePopUpOpen("addSecretImport")}
isDisabled={isReadOnly || isRollbackMode}
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Import
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
</DropdownMenuContent>
@@ -917,10 +963,25 @@ export const DashboardPage = () => {
Go back
</Button>
)}
<ProjectPermissionCan
I={
isRollbackMode
? ProjectPermissionActions.Create
: (ProjectPermissionActions.Edit as any)
}
a={
isRollbackMode
? ProjectPermissionSub.SecretRollback
: ProjectPermissionSub.Secrets
}
>
{(isAllowed) => (
<Button
isDisabled={isSubmitDisabled}
isDisabled={isSubmitDisabled || !isAllowed}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
leftIcon={
<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />
}
onClick={handleSubmit(onSaveSecret)}
className="h-10 text-black"
color="primary"
@@ -928,6 +989,8 @@ export const DashboardPage = () => {
>
{isRollbackMode ? "Rollback" : "Save Changes"}
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<div
@@ -986,14 +1049,22 @@ export const DashboardPage = () => {
{!isReadOnly && !isRollbackMode && (
<tr>
<td colSpan={3} className="hover:bg-mineshaft-700">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<button
type="button"
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
onClick={onAppendSecret}
disabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
<span className="ml-2 w-20">Add Secret</span>
</button>
)}
</ProjectPermissionCan>
</td>
</tr>
)}
@@ -1154,4 +1225,6 @@ export const DashboardPage = () => {
)}
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets }
);
@@ -2,7 +2,9 @@ import { memo } from "react";
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
type Props = {
folders?: Array<{ id: string; name: string }>;
@@ -47,12 +49,18 @@ export const FolderSection = memo(
{name}
</div>
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Folders}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings" className="capitalize">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
isDisabled={!isAllowed}
onClick={() => handleFolderUpdate(id, name)}
ariaLabel="expand"
>
@@ -60,6 +68,13 @@ export const FolderSection = memo(
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Folders}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete" className="capitalize">
<IconButton
@@ -67,12 +82,15 @@ export const FolderSection = memo(
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={!isAllowed}
onClick={() => handleFolderDelete(id, name)}
>
<FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</td>
</tr>
@@ -2,6 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form";
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Drawer,
@@ -14,6 +15,7 @@ import {
Switch,
TextArea
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { FormData, SecretActionType } from "../../DashboardPage.utils";
@@ -85,12 +87,24 @@ export const SecretDetailDrawer = ({
</Button>
</div>
<div className="flex w-full space-x-2">
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly}>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
Save Changes
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<Button
colorSchema="danger"
isDisabled={isReadOnly}
isDisabled={isReadOnly || !isAllowed}
onClick={() => {
const secret = getValues(`secrets.${index}`);
@@ -99,6 +113,8 @@ export const SecretDetailDrawer = ({
>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
}
@@ -32,6 +32,8 @@ import {
Skeleton,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
@@ -78,7 +80,8 @@ type Props = {
decryptFileKey: UserWsKeyPair;
};
export const SecretDropzone = ({
export const SecretDropzone = withProjectPermission(
({
isSmaller,
onParsedEnv,
onAddNewSecret,
@@ -175,7 +178,7 @@ export const SecretDropzone = ({
e.dataTransfer.dropEffect = "copy";
setDragActive.off();
parseFile(e.dataTransfer.files[0], e.dataTransfer?.files?.[0]?.type === "application/json");
parseFile(e.dataTransfer.files[0]);
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
@@ -223,7 +226,12 @@ export const SecretDropzone = ({
>
{isLoading ? (
<div className="mb-16 flex items-center justify-center pt-16">
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="loading animation"
/>
</div>
) : (
<form onSubmit={handleSubmit(handleFormSubmit)}>
@@ -361,7 +369,9 @@ export const SecretDropzone = ({
<Checkbox
id={`pull-secret-${_id}`}
isChecked={Boolean(value)}
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
onCheckedChange={(isChecked) =>
onChange(isChecked ? secVal : "")
}
>
{key}
</Checkbox>
@@ -407,4 +417,6 @@ export const SecretDropzone = ({
)}
</div>
);
};
},
{ action: ProjectPermissionActions.Create, subject: ProjectPermissionSub.Secrets }
);
@@ -9,9 +9,10 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks/useToggle";
import { ProjectPermissionActions, ProjectPermissionSub,useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
@@ -49,7 +50,9 @@ export const SecretImportItem = ({
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
useEffect(() => {
const filteredSecrets = importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
const filteredSecrets = importedSecrets.filter((secret) =>
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
);
if (filteredSecrets.length > 0 && searchTerm) {
setIsExpanded.on();
@@ -58,7 +61,6 @@ export const SecretImportItem = ({
}
}, [searchTerm]);
useEffect(() => {
if (isDragging) {
setIsExpanded.off();
@@ -78,7 +80,11 @@ export const SecretImportItem = ({
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
onClick={() => setIsExpanded.toggle()}
>
<td className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${isExpanded && "border-t-2 border-mineshaft-500"}`}>
<td
className={`ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4 ${
isExpanded && "border-t-2 border-mineshaft-500"
}`}
>
<Tooltip content="Secret Import" className="capitalize">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
</Tooltip>
@@ -106,6 +112,11 @@ export const SecretImportItem = ({
</IconButton>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SecretImports}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete" className="capitalize">
<IconButton
@@ -113,6 +124,7 @@ export const SecretImportItem = ({
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={!isAllowed}
onClick={(evt) => {
evt.stopPropagation();
onDelete(importedEnv, importedSecPath);
@@ -122,12 +134,17 @@ export const SecretImportItem = ({
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</td>
</tr>
<tr>
{isExpanded && !isDragging && (
<td colSpan={3} className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}>
<td
colSpan={3}
className={`bg-bunker-800 ${isExpanded && "border-b-2 border-mineshaft-500"}`}
>
<div className="rounded-md bg-bunker-700 p-1">
<TableContainer>
<table className="secret-table">
@@ -146,7 +163,11 @@ export const SecretImportItem = ({
</td>
</tr>
)}
{importedSecrets.filter(secret => secret.key.toUpperCase().includes(searchTerm.toUpperCase())).map(({ key, value, overriden }, index) => (
{importedSecrets
.filter((secret) =>
secret.key.toUpperCase().includes(searchTerm.toUpperCase())
)
.map(({ key, value, overriden }, index) => (
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
@@ -155,7 +176,10 @@ export const SecretImportItem = ({
<SecretInput value={value} isDisabled isVisible />
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
<EnvFolderIcon
env={overriden?.env}
secretPath={overriden?.secretPath}
/>
</td>
</tr>
))}
@@ -22,6 +22,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { cx } from "cva";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
HoverCard,
HoverCardContent,
@@ -34,6 +35,10 @@ import {
Tag,
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { WsTag } from "@app/hooks/api/types";
@@ -452,6 +457,11 @@ export const SecretInputRow = memo(
</Tooltip>
</div>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
@@ -459,7 +469,7 @@ export const SecretInputRow = memo(
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode}
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
onClick={() => {
onSecretDelete(index, secKey, secId, idOverride);
}}
@@ -468,6 +478,8 @@ export const SecretInputRow = memo(
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</div>
</td>
@@ -1,8 +1,10 @@
import {
LogsSection
} from "./components";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
export const AuditLogsPage = () => {
import { LogsSection } from "./components";
export const AuditLogsPage = withProjectPermission(
() => {
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
@@ -14,4 +16,6 @@ export const AuditLogsPage = () => {
</div>
</div>
);
}
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
);
@@ -1,6 +1,10 @@
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { IPAllowlistSection } from "./components";
export const IPAllowlistPage = () => {
export const IPAllowlistPage = withProjectPermission(
() => {
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">
@@ -12,4 +16,6 @@ export const IPAllowlistPage = () => {
</div>
</div>
);
}
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.IpAllowList }
);
@@ -2,15 +2,15 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription,useWorkspace } from "@app/context";
import {
useDeleteTrustedIp
} from "@app/hooks/api";
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { useDeleteTrustedIp } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { IPAllowlistModal } from "./IPAllowlistModal";
@@ -30,7 +30,6 @@ export const IPAllowlistSection = () => {
const onDeleteTrustedIpSubmit = async (trustedIpId: string) => {
try {
if (!currentWorkspace?._id) return;
await mutateAsync({
@@ -51,28 +50,34 @@ export const IPAllowlistSection = () => {
type: "error"
});
}
}
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
IP Allowlist
</h2>
<h2 className="text-xl font-semibold flex-1 text-white">IP Allowlist</h2>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.IpAllowList}
>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription?.ipAllowlisting) {
handlePopUpOpen("trustedIp")
handlePopUpOpen("trustedIp");
} else {
handlePopUpOpen("upgradePlan");
}
}}
colorSchema="secondary"
isLoading={false}
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add IP
</Button>
)}
</ProjectPermissionCan>
</div>
<IPAllowlistTable
popUp={popUp}
@@ -92,7 +97,9 @@ export const IPAllowlistSection = () => {
onChange={(isOpen) => handlePopUpToggle("deleteTrustedIp", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteTrustedIpSubmit((popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId)
onDeleteTrustedIpSubmit(
(popUp?.deleteTrustedIp?.data as { trustedIpId: string })?.trustedIpId
)
}
/>
<UpgradePlanModal
@@ -102,4 +109,4 @@ export const IPAllowlistSection = () => {
/>
</div>
);
}
};
@@ -1,6 +1,7 @@
import { faGlobe, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
@@ -14,10 +15,13 @@ import {
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription, useWorkspace } from "@app/context";
import {
useGetTrustedIps
} from "@app/hooks/api";
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { useGetTrustedIps } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -30,23 +34,21 @@ type Props = {
comment?: string;
isActive?: boolean;
prefix?: number;
},
}
) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["upgradePlan"]>, state?: boolean) => void;
};
export const IPAllowlistTable = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
export const IPAllowlistTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetTrustedIps(currentWorkspace?._id ?? "");
const formatType = (type: string, prefix?: number) => {
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${(prefix !== undefined) ? "CIDR" : ""}`;
}
return `${type.slice(0, 2).toUpperCase() + type.slice(2)} ${
prefix !== undefined ? "CIDR" : ""
}`;
};
return (
<div>
@@ -62,30 +64,17 @@ export const IPAllowlistTable = ({
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data
{!isLoading &&
data &&
data?.length > 0 &&
data
.sort((a, b) => a.ipAddress.localeCompare(b.ipAddress))
.map(({
_id,
ipAddress,
comment,
type,
prefix,
isActive
}) => {
.map(({ _id, ipAddress, comment, type, prefix, isActive }) => {
return (
<Tr
key={`ip-access-range-${_id}`}
className="h-10"
>
<Td>
{`${ipAddress}${(prefix !== undefined) ? `/${prefix}` : ""}`}
</Td>
<Td>
{formatType(type, prefix)}
</Td>
<Td>
{comment}
</Td>
<Tr key={`ip-access-range-${_id}`} className="h-10">
<Td>{`${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`}</Td>
<Td>{formatType(type, prefix)}</Td>
<Td>{comment}</Td>
{/* <Td>
<div className="flex items-center">
<FontAwesomeIcon
@@ -96,6 +85,11 @@ export const IPAllowlistTable = ({
</div>
</Td> */}
<Td className="flex items-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.IpAllowList}
>
{(isAllowed) => (
<IconButton
className="mr-3 py-2"
onClick={() => {
@@ -114,9 +108,17 @@ export const IPAllowlistTable = ({
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.IpAllowList}
>
{(isAllowed) => (
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
@@ -131,21 +133,23 @@ export const IPAllowlistTable = ({
colorSchema="danger"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton innerKey="ip-access-table" columns={4} key="ip-access-ranges" />}
{isLoading && (
<TableSkeleton innerKey="ip-access-table" columns={4} key="ip-access-ranges" />
)}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No IP addresses added"
icon={faGlobe}
/>
<EmptyState title="No IP addresses added" icon={faGlobe} />
</Td>
</Tr>
)}
@@ -159,4 +163,4 @@ export const IPAllowlistTable = ({
/>
</div>
);
}
};
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useGetRoles } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
@@ -15,7 +16,8 @@ enum TabSections {
Roles = "roles"
}
export const MembersPage = () => {
export const MembersPage = withProjectPermission(
() => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
@@ -29,13 +31,13 @@ export const MembersPage = () => {
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">{t("settings.members.title")}</p>
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
{t("settings.members.title")}
</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>Members</Tab>
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
<Tab value={TabSections.Roles}>Roles</Tab>
)}
</TabList>
<TabPanel value={TabSections.Member}>
<motion.div
@@ -49,10 +51,15 @@ export const MembersPage = () => {
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Roles}>
<ProjectRoleListTab roles={roles as TRole<string>[]} isRolesLoading={isRolesLoading} />
<ProjectRoleListTab
roles={roles as TRole<string>[]}
isRolesLoading={isRolesLoading}
/>
</TabPanel>
</Tabs>
</div>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Member }
);
@@ -7,7 +7,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
@@ -36,6 +36,8 @@ import {
import {
GeneralPermissionActions,
OrgPermissionSubjects,
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useUser,
useWorkspace
@@ -240,7 +242,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
placeholder="Search members..."
/>
</div>
<OrgPermissionCan I={GeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
@@ -250,7 +252,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
Add Member
</Button>
)}
</OrgPermissionCan>
</ProjectPermissionCan>
</div>
<div>
<TableContainer>
@@ -276,9 +278,9 @@ export const MemberListTab = ({ roles = [] }: Props) => {
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<OrgPermissionCan
I={GeneralPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<>
@@ -316,13 +318,13 @@ export const MemberListTab = ({ roles = [] }: Props) => {
)}
</>
)}
</OrgPermissionCan>
</ProjectPermissionCan>
</Td>
<Td>
{userId !== u?._id && (
<OrgPermissionCan
I={GeneralPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<IconButton
@@ -336,7 +338,7 @@ export const MemberListTab = ({ roles = [] }: Props) => {
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</ProjectPermissionCan>
)}
</Td>
</Tr>
@@ -1,5 +1,7 @@
import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { TRole } from "@app/hooks/api/roles/types";
@@ -11,7 +13,8 @@ type Props = {
isRolesLoading?: boolean;
};
export const ProjectRoleListTab = ({ roles = [], isRolesLoading }: Props) => {
export const ProjectRoleListTab = withProjectPermission(
({ roles = [], isRolesLoading }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
return popUp.editRole.isOpen ? (
@@ -42,4 +45,6 @@ export const ProjectRoleListTab = ({ roles = [], isRolesLoading }: Props) => {
/>
</motion.div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Role }
);
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
@@ -19,7 +20,12 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteRole } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
@@ -69,9 +75,17 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
placeholder="Search roles..."
/>
</div>
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => onSelectRole()}
isDisabled={!isAllowed}
>
Add Role
</Button>
)}
</ProjectPermissionCan>
</div>
<div>
<TableContainer>
@@ -99,8 +113,15 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
</Td>
<Td>
<div className="flex space-x-2 items-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<div>
<Tooltip content="Edit">
<IconButton
isDisabled={!isAllowed}
ariaLabel="edit"
onClick={() => onSelectRole(role)}
variant="plain"
@@ -108,19 +129,33 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<div>
<Tooltip
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
content={
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
}
>
<IconButton
ariaLabel="delete"
onClick={() => handlePopUpOpen("deleteRole", role)}
variant="plain"
isDisabled={isNonMutatable}
isDisabled={isNonMutatable || !isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</Td>
</Tr>
);
@@ -34,6 +34,7 @@ import {
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SingleProjectPermission } from "./SingleProjectPermission";
import { WsProjectPermission } from "./WsProjectPermission";
const SINGLE_PERMISSION_LIST = [
{
@@ -271,6 +272,13 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
/>
</div>
))}
<div className="flex flex-col space-y-4" key="permission-ws">
<WsProjectPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}
/>
</div>
</div>
<div className="flex items-center space-x-4 mt-12">
<Button
@@ -0,0 +1,127 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "edit", label: "Update" },
{ action: "delete", label: "Remove" }
] as const;
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.workspace"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue("permissions.workspace", { edit: true, delete: true }, { shouldDirty: true });
break;
default:
setValue("permissions.workspace", { edit: false, delete: false }, { shouldDirty: true });
break;
}
};
return (
<div
className={twMerge(
"px-10 py-6 bg-mineshaft-800 rounded-md",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Workspace</div>
<div className="text-xs font-light">Workspace control actions</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -23,7 +23,13 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useCreateSecretV3,
useDeleteSecretV3,
@@ -38,7 +44,7 @@ import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
export const SecretOverviewPage = () => {
const SecretOverview = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
@@ -394,3 +400,8 @@ export const SecretOverviewPage = () => {
</div>
);
};
export const SecretOverviewPage = withProjectPermission(SecretOverview, {
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Secrets
});
@@ -3,7 +3,9 @@ import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-i
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
type Props = {
@@ -91,19 +93,26 @@ export const SecretEditRow = ({
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
{isDirty ? (
<>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<div>
<Tooltip content="save">
<IconButton
variant="plain"
ariaLabel="submit-value"
className="h-full"
isDisabled={isSubmitting}
isDisabled={isSubmitting || !isAllowed}
onClick={handleSubmit(handleFormSubmit)}
>
<FontAwesomeIcon icon={faCheck} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<div>
<Tooltip content="cancel">
<IconButton
@@ -132,6 +141,11 @@ export const SecretEditRow = ({
</IconButton>
</Tooltip>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
@@ -139,12 +153,14 @@ export const SecretEditRow = ({
ariaLabel="delete-value"
className="h-full"
onClick={handleDeleteSecret}
isDisabled={isDeleting}
isDisabled={isDeleting || !isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</>
)}
</div>
@@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
@@ -14,7 +16,8 @@ const tabs = [
{ name: "Webhooks", key: "tab-project-webhooks" }
];
export const ProjectSettingsPage = () => {
export const ProjectSettingsPage = withProjectPermission(
() => {
const { t } = useTranslation();
return (
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
@@ -32,7 +35,9 @@ export const ProjectSettingsPage = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -55,4 +60,6 @@ export const ProjectSettingsPage = () => {
</div>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
@@ -1,11 +1,14 @@
import { useTranslation } from "react-i18next";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Checkbox } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useToggleAutoCapitalization } from "@app/hooks/api";
export const AutoCapitalizationSection = () => {
export const AutoCapitalizationSection = withProjectPermission(
() => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
@@ -32,14 +35,17 @@ export const AutoCapitalizationSection = () => {
type: "error"
});
}
}
};
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.autoCapitalization ?? false}
onCheckedChange={(state) => {
handleToggleCapitalizationToggle(state as boolean);
@@ -47,6 +53,10 @@ export const AutoCapitalizationSection = () => {
>
{t("settings.project.auto-capitalization-description")}
</Checkbox>
)}
</ProjectPermissionCan>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
@@ -3,19 +3,23 @@ import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import {
useDeleteWorkspace
} from "@app/hooks/api";
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { useDeleteWorkspace } from "@app/hooks/api";
export const DeleteProjectSection = () => {
const { t } = useTranslation();
const router = useRouter();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization()
const { currentOrg } = useOrganization();
const [isDeleting, setIsDeleting] = useToggle();
const [deleteProjectInput, setDeleteProjectInput] = useState("");
const deleteWorkspace = useDeleteWorkspace();
@@ -66,17 +70,21 @@ export const DeleteProjectSection = () => {
/>
</FormControl>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
colorSchema="danger"
onClick={onDeleteWorkspace}
isDisabled={deleteProjectInput !== currentWorkspace?.name || isDeleting}
isDisabled={!isAllowed || deleteProjectInput !== currentWorkspace?.name || isDeleting}
isLoading={isDeleting}
>
{t("settings.project.delete-project")}
</Button>
)}
</ProjectPermissionCan>
<p className="mt-3 ml-0.5 text-xs text-gray-500">
{t("settings.project.delete-project-note")}
</p>
</div>
);
}
};
@@ -1,12 +1,15 @@
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
export const E2EESection = () => {
export const E2EESection = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
@@ -78,22 +81,35 @@ export const E2EESection = () => {
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
<p className="text-gray-400 mb-8">
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to cloud providers as well as HTTP calls to get secrets back raw but enables the server to read/decrypt your secret values.
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
read/decrypt your secret values.
</p>
<p className="text-gray-400 mb-8">
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
)}
</ProjectPermissionCan>
</div>
) : <div />;
};
) : (
<div />
);
},
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Settings
}
);
@@ -2,29 +2,34 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
Button,
DeleteActionModal,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription,useWorkspace } from "@app/context";
import {
useDeleteWsEnvironment
} from "@app/hooks/api";
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeleteWsEnvironment } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddEnvironmentModal } from "./AddEnvironmentModal";
import { EnvironmentTable } from "./EnvironmentTable";
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
export const EnvironmentSection = () => {
export const EnvironmentSection = withProjectPermission(
() => {
const { createNotification } = useNotificationContext();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const deleteWsEnvironment = useDeleteWsEnvironment();
const isMoreEnvironmentsAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true;
const isMoreEnvironmentsAllowed =
subscription?.environmentLimit && currentWorkspace?.environments
? currentWorkspace.environments.length < subscription.environmentLimit
: true;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"createEnv",
@@ -60,10 +65,13 @@ export const EnvironmentSection = () => {
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Environments
</p>
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
@@ -74,17 +82,19 @@ export const EnvironmentSection = () => {
handlePopUpOpen("upgradePlan");
}
}}
isDisabled={!isAllowed}
>
Create environment
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<p className="text-gray-400 mb-8">
Choose which environments will show up in your dashboard like development, staging, production
Choose which environments will show up in your dashboard like development, staging,
production
</p>
<EnvironmentTable
handlePopUpOpen={handlePopUpOpen}
/>
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
<AddEnvironmentModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -113,4 +123,6 @@ export const EnvironmentSection = () => {
/>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Environments }
);
@@ -2,6 +2,7 @@ import { faArrowDown,faArrowUp, faPencil, faXmark } from "@fortawesome/free-soli
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
@@ -14,7 +15,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import {
useReorderWsEnvironment
} from "@app/hooks/api";
@@ -115,17 +116,28 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
>
<FontAwesomeIcon icon={faArrowUp} />
</IconButton>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<IconButton
className="mr-3 py-2"
onClick={() => {
handlePopUpOpen("updateEnv", { name, slug });
}}
isDisabled={!isAllowed}
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteEnv", { name, slug });
@@ -134,9 +146,12 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
colorSchema="danger"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
))}
@@ -3,7 +3,8 @@ import {
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useGetUserWsKey,
useGetWorkspaceIndexStatus,
@@ -11,9 +12,15 @@ import {
useNameWorkspaceSecrets
} from "@app/hooks/api";
export const ProjectIndexSecretsSection = () => {
// TODO: add check so that this only shows up if user is
// an admin in the workspace
export const ProjectIndexSecretsSection = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(currentWorkspace?._id ?? "");
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?._id ?? ""
);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
@@ -50,22 +57,21 @@ export const ProjectIndexSecretsSection = () => {
});
};
return (!isBlindIndexedLoading && (isBlindIndexed === false)) ? (
return !isBlindIndexedLoading && !isBlindIndexed ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
<p className="text-gray-400 mb-8">
Your project, created before the introduction of blind indexing, contains unindexed secrets. To access individual secrets by name through the SDK and public API, please enable blind indexing.
Your project, created before the introduction of blind indexing, contains unindexed
secrets. To access individual secrets by name through the SDK and public API, please
enable blind indexing.
</p>
<Button
onClick={onEnableBlindIndices}
color="mineshaft"
size="sm"
type="submit"
>
<Button onClick={onEnableBlindIndices} color="mineshaft" size="sm" type="submit">
Enable Blind Indexing
</Button>
</div>
) : (
<div />
)
}
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
@@ -4,11 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useRenameWorkspace
} from "@app/hooks/api";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useRenameWorkspace } from "@app/hooks/api";
const formSchema = yup.object({
name: yup.string().required().label("Project Name")
@@ -21,11 +20,7 @@ export const ProjectNameChangeSection = () => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useRenameWorkspace();
const {
handleSubmit,
control,
reset
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
useEffect(() => {
if (currentWorkspace) {
@@ -33,7 +28,6 @@ export const ProjectNameChangeSection = () => {
name: currentWorkspace.name
});
}
}, [currentWorkspace]);
const onFormSubmit = async ({ name }: FormData) => {
@@ -49,7 +43,6 @@ export const ProjectNameChangeSection = () => {
text: "Successfully renamed workspace",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
@@ -64,33 +57,31 @@ export const ProjectNameChangeSection = () => {
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Project Name
</h2>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Project name"
{...field}
className="bg-mineshaft-800"
/>
<Input placeholder="Project name" {...field} className="bg-mineshaft-800" />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
isDisabled={isLoading || !isAllowed}
>
Save
</Button>
)}
</ProjectPermissionCan>
</form>
);
};
@@ -2,10 +2,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteWsTag } from "@app/hooks/api";
@@ -14,7 +14,8 @@ import { SecretTagsTable } from "./SecretTagsTable";
type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = (): JSX.Element => {
export const SecretTagsSection = withProjectPermission(
(): JSX.Element => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"CreateSecretTag",
@@ -48,6 +49,8 @@ export const SecretTagsSection = (): JSX.Element => {
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
@@ -56,17 +59,18 @@ export const SecretTagsSection = (): JSX.Element => {
handlePopUpOpen("CreateSecretTag");
console.log("x2");
}}
isDisabled={!isAllowed}
>
Create tag
</Button>
)}
</ProjectPermissionCan>
</div>
<p className="text-gray-400 mb-8">
Every secret can be assigned to one or more tags. Here you can add and remove tags for
the current project.
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
current project.
</p>
<SecretTagsTable
handlePopUpOpen={handlePopUpOpen}
/>
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
<AddSecretTagModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -84,4 +88,6 @@ export const SecretTagsSection = (): JSX.Element => {
/>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Tags }
);
@@ -1,6 +1,7 @@
import { faTags, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
@@ -13,7 +14,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetWsTags } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -53,6 +54,11 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<IconButton
onClick={() =>
handlePopUpOpen("deleteTagConfirmation", {
@@ -62,9 +68,12 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
}
colorSchema="danger"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
))}
@@ -3,7 +3,10 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteServiceToken } from "@app/hooks/api";
@@ -12,7 +15,8 @@ import { ServiceTokenTable } from "./ServiceTokenTable";
type DeleteModalData = { name: string; id: string };
export const ServiceTokenSection = () => {
export const ServiceTokenSection = withProjectPermission(
() => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const deleteServiceToken = useDeleteServiceToken();
@@ -48,15 +52,23 @@ export const ServiceTokenSection = () => {
<p className="text-xl font-semibold text-mineshaft-100">
{t("section.token.service-tokens")}
</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.ServiceTokens}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("createAPIToken");
}}
isDisabled={!isAllowed}
>
Create token
</Button>
)}
</ProjectPermissionCan>
</div>
<p className="mb-8 text-gray-400">{t("section.token.service-tokens-description")}</p>
<ServiceTokenTable handlePopUpOpen={handlePopUpOpen} />
@@ -73,4 +85,6 @@ export const ServiceTokenSection = () => {
/>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
);
@@ -1,6 +1,7 @@
import { faFolder, faKey, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
@@ -13,7 +14,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetUserWsServiceTokens } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -70,6 +71,11 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
</Td>
<Td>{row.expiresAt && new Date(row.expiresAt).toUTCString()}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.ServiceTokens}
>
{(isAllowed) => (
<IconButton
onClick={() =>
handlePopUpOpen("deleteAPITokenConfirmation", {
@@ -79,9 +85,12 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
}
colorSchema="danger"
ariaLabel="delete"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
))}
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
@@ -17,7 +18,8 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
useCreateWebhook,
@@ -29,7 +31,8 @@ import {
import { AddWebhookForm, TFormSchema } from "./AddWebhookForm";
export const WebhooksTab = () => {
export const WebhooksTab = withProjectPermission(
() => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
@@ -139,12 +142,20 @@ export const WebhooksTab = () => {
<div className="mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">{t("settings.webhooks.title")}</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Webhooks}
>
{(isAllowed) => (
<Button
onClick={() => handlePopUpOpen("addWebhook")}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Create
</Button>
)}
</ProjectPermissionCan>
</div>
<p className="mb-8 text-gray-400">{t("settings.webhooks.description")}</p>
<div>
@@ -219,23 +230,41 @@ export const WebhooksTab = () => {
</Td>
<Td>
<div className="flex items-center justify-end space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Webhooks}
>
{(isAllowed) => (
<Button
variant="star"
size="xs"
onClick={() => handleWebhookTest(id)}
isDisabled={
(isTestWebhookSubmitting &&
testWebhookVars?.webhookId === id) ||
!isAllowed
}
isLoading={
isTestWebhookSubmitting && testWebhookVars?.webhookId === id
}
isLoading={isTestWebhookSubmitting && testWebhookVars?.webhookId === id}
>
Test
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Webhooks}
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
onClick={() => handleWebhookDisable(id, !isDisabled)}
isDisabled={
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
(isUpdateWebhookSubmitting &&
updateWebhookVars?.webhookId === id) ||
!isAllowed
}
isLoading={
isUpdateWebhookSubmitting && updateWebhookVars?.webhookId === id
@@ -243,15 +272,25 @@ export const WebhooksTab = () => {
>
{isDisabled ? "Enable" : "Disable"}
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Webhooks}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
colorSchema="danger"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("deleteWebhook", id)}
>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>
</Td>
</Tr>
@@ -277,4 +316,6 @@ export const WebhooksTab = () => {
/>
</div>
);
};
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Webhooks }
);