diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 840c2d504..6f6411541 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -201,14 +201,6 @@ export const logout = async (req: Request, res: Response) => { }); }; -export const getCommonPasswords = async (req: Request, res: Response) => { - const commonPasswords = fs - .readFileSync(path.resolve(__dirname, "../../data/" + "common_passwords.txt"), "utf8") - .split("\n"); - - return res.status(200).send(commonPasswords); -}; - export const revokeAllSessions = async (req: Request, res: Response) => { await TokenVersion.updateMany( { diff --git a/backend/src/controllers/v1/roleController.ts b/backend/src/controllers/v1/roleController.ts index c1fe7d66f..e290c6a6e 100644 --- a/backend/src/controllers/v1/roleController.ts +++ b/backend/src/controllers/v1/roleController.ts @@ -203,7 +203,6 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) = params: { workspaceId } } = await validateRequest(GetUserProjectPermission, req); const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); - res.status(200).json({ data: { permissions: packRules(permission.rules) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index b703c9d50..4aa95d574 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -77,16 +77,6 @@ export const batchSecrets = async (req: Request, res: Response) => { const folders = await Folder.findOne({ workspace: workspaceId, environment }); - if (req.authData.authPayload instanceof ServiceTokenData) { - await validateServiceTokenDataClientForWorkspace({ - serviceTokenData: req.authData.authPayload, - workspaceId: new Types.ObjectId(workspaceId), - environment, - secretPath, - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }); - } - if (secretPath) { folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); } @@ -100,7 +90,15 @@ export const batchSecrets = async (req: Request, res: Response) => { ); } - if (req.user?._id) { + if (req.authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: req.authData.authPayload, + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + requiredPermissions: [PERMISSION_WRITE_SECRETS] + }); + } else { const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index cefd5a131..530797379 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -44,6 +44,4 @@ const membershipOrgSchema = new Schema( } ); -const MembershipOrg = model("MembershipOrg", membershipOrgSchema); - -export default MembershipOrg; +export const MembershipOrg = model("MembershipOrg", membershipOrgSchema); diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 42a59ffb1..a3037f311 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -38,8 +38,6 @@ router.post( authController.checkAuth ); -router.get("/common-passwords", authLimiter, authController.getCommonPasswords); - router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) "/sessions", diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 9a260c538..cfb27ee71 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -93,11 +93,11 @@ router.get( ); router.get( - "/:integrationAuthId/teamcity/build-configs", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), - integrationAuthController.getIntegrationAuthTeamCityBuildConfigs + "/:integrationAuthId/teamcity/build-configs", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + integrationAuthController.getIntegrationAuthTeamCityBuildConfigs ); router.delete( diff --git a/backend/src/services/ProjectRoleService.ts b/backend/src/services/ProjectRoleService.ts index f3e8b754a..9a86c048e 100644 --- a/backend/src/services/ProjectRoleService.ts +++ b/backend/src/services/ProjectRoleService.ts @@ -219,7 +219,6 @@ export const getUserProjectPermissions = async (userId: string, workspaceId: str }>("customRole") .exec(); - console.log(membership, userId, workspaceId); if (!membership || (membership.role === "custom" && !membership.customRole)) { throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); } diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index c46cb2016..233805545 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -6,7 +6,6 @@ import { MembershipNotFoundError } from "../utils/errors"; import { AuthData } from "../interfaces/middleware"; import { ActorType } from "../ee/models"; import { z } from "zod"; -import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; /** * Validate authenticated clients for membership with id [membershipId] based @@ -66,7 +65,7 @@ export const DeleteMembershipV1 = z.object({ export const ChangeMembershipRoleV1 = z.object({ body: z.object({ - role: z.enum([ADMIN, VIEWER, MEMBER, CUSTOM]) + role: z.string().trim() }), params: z.object({ membershipId: z.string().trim() }) }); diff --git a/backend/src/validation/role.ts b/backend/src/validation/role.ts index 734ea73f5..61252a39d 100644 --- a/backend/src/validation/role.ts +++ b/backend/src/validation/role.ts @@ -11,7 +11,7 @@ export const CreateRoleSchema = z.object({ .object({ subject: z.string(), action: z.string(), - condition: z.record(z.union([z.string(), z.number()])).optional() + conditions: z.record(z.union([z.string(), z.number()])).optional() }) .array() }) @@ -31,7 +31,7 @@ export const UpdateRoleSchema = z.object({ .object({ subject: z.string(), action: z.string(), - condition: z.record(z.union([z.string(), z.number()])).optional() + conditions: z.record(z.union([z.string(), z.number()])).optional() }) .array() .optional() diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index b4dd2f508..fbdda2516 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -18,13 +18,13 @@ export type TRole = { export type TPermission = TWorkspacePermission | TGeneralPermission; type TGeneralPermission = { - condition?: Record; + conditions?: Record; action: "read" | "edit" | "create" | "delete"; subject: "member" | "role" | "incident-contact" | "sso" | "billing" | "settings"; }; type TWorkspacePermission = { - condition?: Record; + conditions?: Record; action: "read" | "create"; subject: "workspace"; }; @@ -32,7 +32,7 @@ type TWorkspacePermission = { export type TProjectPermission = TProjectGeneralPermission | TProjectWorkspacePermission; type TProjectGeneralPermission = { - condition?: Record; + conditions?: Record; action: "read" | "edit" | "create" | "delete"; subject: | "member" @@ -46,7 +46,7 @@ type TProjectGeneralPermission = { }; type TProjectWorkspacePermission = { - condition?: Record; + conditions?: Record; action: "delete" | "edit"; subject: "workspace"; }; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 371694bd5..efd58f578 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; +import { subject } from "@casl/ability"; import { closestCenter, DndContext, @@ -25,6 +26,7 @@ import { faEyeSlash, faFileImport, faFolderPlus, + faLock, faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons"; @@ -60,6 +62,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useOrganization, + useProjectPermission, useSubscription, useWorkspace } from "@app/context"; @@ -79,7 +82,6 @@ import { useGetSecretVersion, useGetSnapshotSecrets, useGetUserAction, - useGetUserWsEnvironments, useGetUserWsKey, useGetWorkspaceSecretSnapshots, useGetWsSnapshotCount, @@ -139,6 +141,7 @@ export const DashboardPage = withProjectPermission( const { createNotification } = useNotificationContext(); const queryClient = useQueryClient(); const envQuery = router.query.env as string; + const permission = useProjectPermission(); const secretContainer = useRef(null); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ @@ -181,17 +184,6 @@ export const DashboardPage = withProjectPermission( const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); const hasUserPushed = Boolean(userAction); - const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId, - onSuccess: (data) => { - // get an env with one of the access available - const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); - if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) { - setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]); - } - } - }); - const { data: secretVersion } = useGetSecretVersion({ limit: 10, offset: 0, @@ -267,6 +259,24 @@ export const DashboardPage = withProjectPermission( folderId }); + const secretPath = `/${(folderData?.dir || []) + ?.filter(({ name }) => name !== "root") + .join("/")}`; + + const userAvailableEnvs = currentWorkspace?.environments?.filter(({ slug }) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath }) + ) + ); + + useEffect(() => { + if (!isLoading && currentWorkspace) { + const env = userAvailableEnvs?.find(({ slug }) => slug === envQuery); + if (env) setSelectedEnv(env); + } + }, [isLoading, workspaceId, userAvailableEnvs]); + // This is for dnd-kit. As react-query state mutation async // This will act as a placeholder to avoid a glitching animation on dropping items const [items, setItems] = useState< @@ -316,11 +326,19 @@ export const DashboardPage = withProjectPermission( reset } = method; const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); - const isReadOnly = selectedEnv?.isWriteDenied; - const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; - const canDoRollback = !isReadOnly && !isAddOnly; - const isSubmitDisabled = - isReadOnly || (!isRollbackMode && !isDirty) || isAddOnly || isSubmitting; + + const isReadOnly = + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: selectedEnvSlug }) + ) && + permission.cannot( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: selectedEnvSlug }) + ); + + const canDoRollback = !isReadOnly; + const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isSubmitting; useEffect(() => { if (!isSnapshotChanging && Boolean(snapshotId)) { @@ -437,14 +455,12 @@ export const DashboardPage = withProjectPermission( } // just closing this if save is triggered from drawer handlePopUpClose("secretDetails"); - // when add only mode remove rest of things not created - const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec; // encrypt and format the secrets to batch api format // requests = [ {method:"", secret:""} ] const batchedSecret = transformSecretsToBatchSecretReq( deletedSecretIds.current, latestFileKey, - sec, + userSec, secrets?.secrets ); // type check @@ -486,7 +502,8 @@ export const DashboardPage = withProjectPermission( // eslint-disable-next-line no-alert if (!window.confirm(leaveConfirmDefaultMessage)) return; } - const env = wsEnv?.find((el) => el.slug === slug); + + const env = userAvailableEnvs?.find((el) => el.slug === slug); if (env) setSelectedEnv(env); const query: Record = { ...router.query, env: slug }; delete query.folderId; @@ -748,7 +765,7 @@ export const DashboardPage = withProjectPermission( const isSecretImportEmpty = !secretImportCfg?.imports?.length; const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty; - if (isSecretsLoading || isEnvListLoading) { + if (isSecretsLoading) { return (
loading animation @@ -756,9 +773,29 @@ export const DashboardPage = withProjectPermission( ); } - const userAvailableEnvs = wsEnv?.filter( - ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied - ); + if ( + permission.cannot( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: envQuery, secretPath }) + ) + ) { + return ( +
+
+
+ +
+
+
Permission Denied
+
+ You do not have permission to this page.
Kindly contact your organization + administrator +
+
+
+
+ ); + } return (
@@ -768,7 +805,7 @@ export const DashboardPage = withProjectPermission( envir.slug === envQuery)[0].name || "" + userAvailableEnvs?.filter((envir) => envir.slug === envQuery)?.[0]?.name || "" } isFolderMode folders={folderData?.dir} @@ -869,7 +906,7 @@ export const DashboardPage = withProjectPermission( }} leftIcon={} isLoading={isLoadingSnapshotCount} - isDisabled={!canDoRollback && !isAllowed} + isDisabled={!canDoRollback || !isAllowed} className="h-10" > {snapshotCount} Commits @@ -1037,7 +1074,6 @@ export const DashboardPage = withProjectPermission( secUniqId={_id} isReadOnly={isReadOnly} isRollbackMode={isRollbackMode} - isAddOnly={isAddOnly} index={index} searchTerm={searchFilter} onSecretDelete={onSecretDelete} diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index 2c4ab23d5..557ca0f66 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -58,14 +58,14 @@ const multiEnvApi2Form = ( formVal: TFormSchema["permissions"]["secrets"], permission: TProjectPermission ) => { - const isCustomRule = Boolean(permission?.condition?.slug); + const isCustomRule = Boolean(permission?.conditions?.environment); // full access if (isCustomRule && formVal && !formVal?.custom) { formVal.custom = { read: true, edit: true, delete: true, create: true }; } - const secretEnv = permission?.condition?.slug || "all"; - const secretPath = permission?.condition?.secretPath; + const secretEnv = permission?.conditions?.environment || "all"; + const secretPath = permission?.conditions?.secretPath; // initialize if (formVal && !formVal?.[secretEnv]) { formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath }; @@ -94,7 +94,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { permissions.forEach((permission) => { if (["secrets", "folders", "secret-imports"].includes(permission.subject)) { - multiEnvApi2Form(formVal?.secrets, permission); + multiEnvApi2Form(formVal[permission.subject], permission); } else { // everything else follows same pattern // formVal[settings][read | write] = true @@ -134,11 +134,10 @@ const multiEnvForm2Api = ( actions.forEach((action) => { // if not full access for an action if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) { - permissions.push({ - action, - subject, - condition: { slug, secretPath: formVal[slug]?.secretPath } - }); + const conditions: Record = { environment: slug }; + if (formVal[slug]?.secretPath) conditions.secretPath = formVal[slug].secretPath; + + permissions.push({ action, subject, conditions }); } }); }); diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 234d38ba5..4be813183 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -2,7 +2,13 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; -import { faArrowDown, faArrowUp, faFolderBlank, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; +import { subject } from "@casl/ability"; +import { + faArrowDown, + faArrowUp, + faFolderBlank, + faMagnifyingGlass +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; @@ -27,6 +33,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useOrganization, + useProjectPermission, useWorkspace } from "@app/context"; import { withProjectPermission } from "@app/hoc"; @@ -76,6 +83,7 @@ const SecretOverview = () => { const { data: latestFileKey } = useGetUserWsKey(workspaceId); const [searchFilter, setSearchFilter] = useState(""); const secretPath = router.query?.secretPath as string; + const permission = useProjectPermission(); useEffect(() => { if (!isWorkspaceLoading && !workspaceId && router.isReady) { @@ -83,11 +91,13 @@ const SecretOverview = () => { } }, [isWorkspaceLoading, workspaceId, router.isReady]); - const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId - }); - - const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied) || []; + const userAvailableEnvs = + currentWorkspace?.environments?.filter(({ slug }) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: secretPath || "/" }) + ) + ) || []; const { data: secrets, @@ -213,7 +223,7 @@ const SecretOverview = () => { } }; - if (isEnvListLoading) { + if (isWorkspaceLoading) { return (
loading animation @@ -225,9 +235,9 @@ const SecretOverview = () => { folders?.some(({ isLoading }) => !isLoading) && secrets?.some(({ isLoading }) => !isLoading) ); - const filteredSecretNames = secKeys?.filter((name) => - name.toUpperCase().includes(searchFilter.toUpperCase()) - ).sort((a, b) => sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)); + const filteredSecretNames = secKeys + ?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase())) + .sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))); const filteredFolderNames = folderNames?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()) ); @@ -286,7 +296,12 @@ const SecretOverview = () => {
Name - setSortDir(prev => prev === "asc" ? "desc" : "asc")}> + setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))} + >