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