From b09dddec1c51c961ffa2f9b92005f71843c82721 Mon Sep 17 00:00:00 2001
From: kasyap dharanikota <76957823+kasyap1234@users.noreply.github.com>
Date: Wed, 17 Jul 2024 15:41:36 +0530
Subject: [PATCH 01/30] Add SMTP2GO credentials for email configuration
---
docs/self-hosting/configuration/envars.mdx | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx
index 03bea6fb0..8719fa0e9 100644
--- a/docs/self-hosting/configuration/envars.mdx
+++ b/docs/self-hosting/configuration/envars.mdx
@@ -328,6 +328,25 @@ SMTP_FROM_NAME=Infisical
+
+1. Create an account and configure [SMTP2Go](https://www.smtp2go.com/) to send emails.
+2. Turn on SMTP authentication
+```
+SMTP server: mail.smtp2go.com
+SMTP port: You can use one of the following ports: 2525, 80, 25, 8025, or 587
+Username: Your SMTP2GO account's SMTP username
+Password: Your SMTP2GO account's SMTP password
+```
+{" "}
+
+
+Optional (for TLS/SSL):
+
+TLS: Available on the same ports (2525, 80, 25, 8025, or 587)
+SSL: Available on ports 465, 8465, and 443
+
+
+
## Authentication
By default, users can only login via email/password based login method.
From 8542ec8c3ee52b26fc5d75d2e9300d2f039c198c Mon Sep 17 00:00:00 2001
From: Tuan Dang
Date: Wed, 17 Jul 2024 19:48:04 +0700
Subject: [PATCH 02/30] Complete preliminary user page
---
backend/src/lib/api-docs/constants.ts | 7 +-
backend/src/server/routes/index.ts | 1 +
.../src/server/routes/v1/project-router.ts | 1 +
.../server/routes/v2/organization-router.ts | 128 +++++++-
.../org-membership/org-membership-dal.ts | 49 +++-
backend/src/services/org/org-dal.ts | 5 +-
backend/src/services/org/org-service.ts | 58 +++-
backend/src/services/org/org-types.ts | 11 +-
.../project-membership-dal.ts | 103 ++++++-
frontend/src/hooks/api/users/index.tsx | 7 +-
frontend/src/hooks/api/users/mutation.tsx | 3 +-
frontend/src/hooks/api/users/queries.tsx | 76 ++++-
frontend/src/hooks/api/users/types.ts | 12 +-
frontend/src/hooks/api/workspace/queries.tsx | 5 +-
frontend/src/layouts/AppLayout/AppLayout.tsx | 3 +-
.../[id]/memberships/[membershipId]/index.tsx | 20 ++
.../src/pages/org/[id]/overview/index.tsx | 3 +-
.../IdentityProjectsTable.tsx | 6 +-
.../OrgMembersSection/OrgMembersSection.tsx | 40 ++-
.../OrgMembersSection/OrgMembersTable.tsx | 163 ++++++++---
frontend/src/views/Org/UserPage/UserPage.tsx | 277 ++++++++++++++++++
.../components/UserDetailsSection.tsx | 195 ++++++++++++
.../components/UserOrgMembershipModal.tsx | 160 ++++++++++
.../UserAddToProjectModal.tsx | 145 +++++++++
.../UserProjectsSection/UserProjectRow.tsx | 90 ++++++
.../UserProjectsSection.tsx | 98 +++++++
.../UserProjectsSection/UserProjectsTable.tsx | 62 ++++
.../components/UserProjectsSection/index.tsx | 1 +
.../views/Org/UserPage/components/index.tsx | 3 +
frontend/src/views/Org/UserPage/index.tsx | 1 +
.../MembersTab/components/AddMemberModal.tsx | 8 +-
31 files changed, 1661 insertions(+), 80 deletions(-)
create mode 100644 frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx
create mode 100644 frontend/src/views/Org/UserPage/UserPage.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectRow.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsTable.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/UserProjectsSection/index.tsx
create mode 100644 frontend/src/views/Org/UserPage/components/index.tsx
create mode 100644 frontend/src/views/Org/UserPage/index.tsx
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index e635d93da..5aae289bf 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -348,10 +348,15 @@ export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from."
},
+ GET_USER_MEMBERSHIP: {
+ organizationId: "The ID of the organization to get the membership for.",
+ membershipId: "The ID of the membership to get."
+ },
UPDATE_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to update the membership for.",
membershipId: "The ID of the membership to update.",
- role: "The new role of the membership."
+ role: "The new role of the membership.",
+ isActive: "The active status of the membership"
},
DELETE_USER_MEMBERSHIP: {
organizationId: "The ID of the organization to delete the membership from.",
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 18f2d62b1..a0df46dc5 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -457,6 +457,7 @@ export const registerRoutes = async (
tokenService,
projectDAL,
projectMembershipDAL,
+ orgMembershipDAL,
projectKeyDAL,
smtpService,
userDAL,
diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts
index 99f05cf94..b6c4ce7bd 100644
--- a/backend/src/server/routes/v1/project-router.ts
+++ b/backend/src/server/routes/v1/project-router.ts
@@ -78,6 +78,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
+ project: ProjectsSchema.pick({ name: true, id: true }),
roles: z.array(
z.object({
id: z.string(),
diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts
index 07074eba3..f87e7a6f0 100644
--- a/backend/src/server/routes/v2/organization-router.ts
+++ b/backend/src/server/routes/v2/organization-router.ts
@@ -1,6 +1,13 @@
import { z } from "zod";
-import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
+import {
+ OrganizationsSchema,
+ OrgMembershipsSchema,
+ ProjectMembershipsSchema,
+ ProjectsSchema,
+ UserEncryptionKeysSchema,
+ UsersSchema
+} from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -30,6 +37,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
user: UsersSchema.pick({
username: true,
email: true,
+ isEmailVerified: true,
firstName: true,
lastName: true,
id: true
@@ -103,6 +111,54 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ method: "GET",
+ url: "/:organizationId/memberships/:membershipId",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Get organization user membership",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ organizationId: z.string().trim().describe(ORGANIZATIONS.GET_USER_MEMBERSHIP.organizationId),
+ membershipId: z.string().trim().describe(ORGANIZATIONS.GET_USER_MEMBERSHIP.membershipId)
+ }),
+ response: {
+ 200: z.object({
+ membership: OrgMembershipsSchema.merge(
+ z.object({
+ user: UsersSchema.pick({
+ username: true,
+ email: true,
+ isEmailVerified: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ }).merge(z.object({ publicKey: z.string().nullable() }))
+ })
+ ).omit({ createdAt: true, updatedAt: true })
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const membership = await server.services.org.getOrgMembership({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ orgId: req.params.organizationId,
+ membershipId: req.params.membershipId
+ });
+ return { membership };
+ }
+ });
+
server.route({
method: "PATCH",
url: "/:organizationId/memberships/:membershipId",
@@ -121,7 +177,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
membershipId: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.membershipId)
}),
body: z.object({
- role: z.string().trim().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role)
+ role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
+ isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive)
}),
response: {
200: z.object({
@@ -135,11 +192,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const membership = await server.services.org.updateOrgMembership({
userId: req.permission.id,
- role: req.body.role,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
membershipId: req.params.membershipId,
- actorOrgId: req.permission.orgId
+ actorOrgId: req.permission.orgId,
+ ...req.body
});
return { membership };
}
@@ -183,6 +240,69 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ // TODO: re-think endpoint structure in future so users only need to pass in membershipId bc organizationId is redundant
+ method: "GET",
+ url: "/:organizationId/memberships/:membershipId/project-memberships",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ description: "Get project memberships given organization membership",
+ security: [
+ {
+ bearerAuth: []
+ }
+ ],
+ params: z.object({
+ organizationId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.organizationId),
+ membershipId: z.string().trim().describe(ORGANIZATIONS.DELETE_USER_MEMBERSHIP.membershipId)
+ }),
+ response: {
+ 200: z.object({
+ memberships: ProjectMembershipsSchema.extend({
+ user: UsersSchema.pick({
+ email: true,
+ username: true,
+ firstName: true,
+ lastName: true,
+ id: true
+ }).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
+ project: ProjectsSchema.pick({ name: true, id: true }),
+ roles: z.array(
+ z.object({
+ id: z.string(),
+ role: z.string(),
+ customRoleId: z.string().optional().nullable(),
+ customRoleName: z.string().optional().nullable(),
+ customRoleSlug: z.string().optional().nullable(),
+ isTemporary: z.boolean(),
+ temporaryMode: z.string().optional().nullable(),
+ temporaryRange: z.string().nullable().optional(),
+ temporaryAccessStartTime: z.date().nullable().optional(),
+ temporaryAccessEndTime: z.date().nullable().optional()
+ })
+ )
+ })
+ .omit({ createdAt: true, updatedAt: true })
+ .array()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ const memberships = await server.services.org.listProjectMembershipsByOrgMembershipId({
+ actor: req.permission.type,
+ actorId: req.permission.id,
+ actorAuthMethod: req.permission.authMethod,
+ actorOrgId: req.permission.orgId,
+ orgId: req.params.organizationId,
+ orgMembershipId: req.params.membershipId
+ });
+ return { memberships };
+ }
+ });
+
server.route({
method: "POST",
url: "/",
diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts
index 9990d9c3d..ccd995296 100644
--- a/backend/src/services/org-membership/org-membership-dal.ts
+++ b/backend/src/services/org-membership/org-membership-dal.ts
@@ -1,5 +1,6 @@
import { TDbClient } from "@app/db";
-import { TableName } from "@app/db/schemas";
+import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TOrgMembershipDALFactory = ReturnType;
@@ -7,7 +8,51 @@ export type TOrgMembershipDALFactory = ReturnType {
const orgMembershipOrm = ormify(db, TableName.OrgMembership);
+ const findOrgMembershipById = async (membershipId: string) => {
+ try {
+ const member = await db
+ .replicaNode()(TableName.OrgMembership)
+ .where(`${TableName.OrgMembership}.id`, membershipId)
+ .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
+ .leftJoin(
+ TableName.UserEncryptionKey,
+ `${TableName.UserEncryptionKey}.userId`,
+ `${TableName.Users}.id`
+ )
+ .select(
+ db.ref("id").withSchema(TableName.OrgMembership),
+ db.ref("inviteEmail").withSchema(TableName.OrgMembership),
+ db.ref("orgId").withSchema(TableName.OrgMembership),
+ db.ref("role").withSchema(TableName.OrgMembership),
+ db.ref("roleId").withSchema(TableName.OrgMembership),
+ db.ref("status").withSchema(TableName.OrgMembership),
+ db.ref("isActive").withSchema(TableName.OrgMembership),
+ db.ref("email").withSchema(TableName.Users),
+ db.ref("username").withSchema(TableName.Users),
+ db.ref("firstName").withSchema(TableName.Users),
+ db.ref("lastName").withSchema(TableName.Users),
+ db.ref("isEmailVerified").withSchema(TableName.Users),
+ db.ref("id").withSchema(TableName.Users).as("userId"),
+ db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
+ )
+ .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
+ .first();
+
+ if (!member) return undefined;
+
+ const { email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data } = member;
+
+ return {
+ ...data,
+ user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
+ };
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Find org membership by id" });
+ }
+ };
+
return {
- ...orgMembershipOrm
+ ...orgMembershipOrm,
+ findOrgMembershipById
};
};
diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts
index cb611031c..051869429 100644
--- a/backend/src/services/org/org-dal.ts
+++ b/backend/src/services/org/org-dal.ts
@@ -76,6 +76,7 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("isActive").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
+ db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
@@ -84,9 +85,9 @@ export const orgDALFactory = (db: TDbClient) => {
)
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
- return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
+ return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
- user: { email, username, firstName, lastName, id: userId, publicKey }
+ user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index fe67c9041..3e30d8c52 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -15,9 +15,10 @@ import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
-import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
+import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
+import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
@@ -38,7 +39,9 @@ import {
TFindAllWorkspacesDTO,
TFindOrgMembersByEmailDTO,
TGetOrgGroupsDTO,
+ TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
+ TListProjectMembershipsByOrgMembershipIdDTO,
TUpdateOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
@@ -54,6 +57,7 @@ type TOrgServiceFactoryDep = {
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick;
projectKeyDAL: Pick;
+ orgMembershipDAL: Pick;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick;
smtpService: TSmtpService;
@@ -79,6 +83,7 @@ export const orgServiceFactory = ({
projectDAL,
projectMembershipDAL,
projectKeyDAL,
+ orgMembershipDAL,
tokenService,
orgBotDAL,
licenseService,
@@ -364,6 +369,7 @@ export const orgServiceFactory = ({
* */
const updateOrgMembership = async ({
role,
+ isActive,
orgId,
userId,
membershipId,
@@ -374,7 +380,7 @@ export const orgServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
- if (isCustomRole) {
+ if (role && isCustomRole) {
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!customRole) throw new BadRequestError({ name: "Update membership", message: "Role not found" });
@@ -394,7 +400,11 @@ export const orgServiceFactory = ({
return membership;
}
- const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null });
+ if (isActive !== undefined) {
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
+ }
+
+ const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive });
return membership;
};
/*
@@ -585,6 +595,24 @@ export const orgServiceFactory = ({
return { token, user };
};
+ const getOrgMembership = async ({
+ membershipId,
+ orgId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TGetOrgMembershipDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
+
+ const membership = await orgMembershipDAL.findOrgMembershipById(membershipId);
+ if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" });
+ if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" });
+
+ return membership;
+ };
+
const deleteOrgMembership = async ({
orgId,
userId,
@@ -608,6 +636,26 @@ export const orgServiceFactory = ({
return deletedMembership;
};
+ const listProjectMembershipsByOrgMembershipId = async ({
+ orgMembershipId,
+ orgId,
+ actor,
+ actorId,
+ actorAuthMethod,
+ actorOrgId
+ }: TListProjectMembershipsByOrgMembershipIdDTO) => {
+ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
+ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
+
+ const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId);
+ if (!membership) throw new NotFoundError({ message: "Failed to find organization membership" });
+ if (membership.orgId !== orgId) throw new NotFoundError({ message: "Failed to find organization membership" });
+
+ const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, membership.user.id);
+
+ return projectMemberships;
+ };
+
/*
* CRUD operations of incident contacts
* */
@@ -668,6 +716,7 @@ export const orgServiceFactory = ({
findOrgMembersByUsername,
createOrganization,
deleteOrganizationById,
+ getOrgMembership,
deleteOrgMembership,
findAllWorkspaces,
addGhostUser,
@@ -676,6 +725,7 @@ export const orgServiceFactory = ({
findIncidentContacts,
createIncidentContact,
deleteIncidentContact,
- getOrgGroups
+ getOrgGroups,
+ listProjectMembershipsByOrgMembershipId
};
};
diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts
index 0efc7ffe1..08b4c1c92 100644
--- a/backend/src/services/org/org-types.ts
+++ b/backend/src/services/org/org-types.ts
@@ -6,11 +6,16 @@ export type TUpdateOrgMembershipDTO = {
userId: string;
orgId: string;
membershipId: string;
- role: string;
+ role?: string;
+ isActive?: boolean;
actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod;
};
+export type TGetOrgMembershipDTO = {
+ membershipId: string;
+} & TOrgPermission;
+
export type TDeleteOrgMembershipDTO = {
userId: string;
orgId: string;
@@ -55,3 +60,7 @@ export type TUpdateOrgDTO = {
} & TOrgPermission;
export type TGetOrgGroupsDTO = TOrgPermission;
+
+export type TListProjectMembershipsByOrgMembershipIdDTO = {
+ orgMembershipId: string;
+} & TOrgPermission;
diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts
index 93ec6597e..19503ba3b 100644
--- a/backend/src/services/project-membership/project-membership-dal.ts
+++ b/backend/src/services/project-membership/project-membership-dal.ts
@@ -16,6 +16,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const docs = await db
.replicaNode()(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
+ .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.where((qb) => {
if (filter.usernames) {
@@ -58,17 +59,22 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
- db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole)
+ db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("name").as("projectName").withSchema(TableName.Project)
)
.where({ isGhost: false });
const members = sqlNestRelationships({
data: docs,
- parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId }) => ({
+ parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({
id,
userId,
projectId,
- user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
+ user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
+ project: {
+ id: projectId,
+ name: projectName
+ }
}),
key: "id",
childrenMapper: [
@@ -151,14 +157,95 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const findProjectMembershipsByUserId = async (orgId: string, userId: string) => {
try {
- const memberships = await db
+ const docs = await db
.replicaNode()(TableName.ProjectMembership)
- .where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
- .where({ [`${TableName.Project}.orgId` as "orgId"]: orgId })
- .select(selectAllTableCols(TableName.ProjectMembership));
+ .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
+ .where(`${TableName.Users}.id`, userId)
+ .where(`${TableName.Project}.orgId`, orgId)
+ .join(
+ TableName.UserEncryptionKey,
+ `${TableName.UserEncryptionKey}.userId`,
+ `${TableName.Users}.id`
+ )
+ .join(
+ TableName.ProjectUserMembershipRole,
+ `${TableName.ProjectUserMembershipRole}.projectMembershipId`,
+ `${TableName.ProjectMembership}.id`
+ )
+ .leftJoin(
+ TableName.ProjectRoles,
+ `${TableName.ProjectUserMembershipRole}.customRoleId`,
+ `${TableName.ProjectRoles}.id`
+ )
+ .select(
+ db.ref("id").withSchema(TableName.ProjectMembership),
+ db.ref("isGhost").withSchema(TableName.Users),
+ db.ref("username").withSchema(TableName.Users),
+ db.ref("email").withSchema(TableName.Users),
+ db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
+ db.ref("firstName").withSchema(TableName.Users),
+ db.ref("lastName").withSchema(TableName.Users),
+ db.ref("id").withSchema(TableName.Users).as("userId"),
+ db.ref("role").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"),
+ db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
+ db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
+ db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
+ db.ref("name").as("projectName").withSchema(TableName.Project),
+ db.ref("id").as("projectId").withSchema(TableName.Project)
+ )
+ .where({ isGhost: false });
- return memberships;
+ const members = sqlNestRelationships({
+ data: docs,
+ parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({
+ id,
+ userId,
+ projectId,
+ user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
+ project: {
+ id: projectId,
+ name: projectName
+ }
+ }),
+ key: "id",
+ childrenMapper: [
+ {
+ label: "roles" as const,
+ key: "membershipRoleId",
+ mapper: ({
+ role,
+ customRoleId,
+ customRoleName,
+ customRoleSlug,
+ membershipRoleId,
+ temporaryRange,
+ temporaryMode,
+ temporaryAccessEndTime,
+ temporaryAccessStartTime,
+ isTemporary
+ }) => ({
+ id: membershipRoleId,
+ role,
+ customRoleId,
+ customRoleName,
+ customRoleSlug,
+ temporaryRange,
+ temporaryMode,
+ temporaryAccessEndTime,
+ temporaryAccessStartTime,
+ isTemporary
+ })
+ }
+ ]
+ });
+ return members;
} catch (error) {
throw new DatabaseError({ error, name: "Find project memberships by user id" });
}
diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx
index 521c36468..2026cede1 100644
--- a/frontend/src/hooks/api/users/index.tsx
+++ b/frontend/src/hooks/api/users/index.tsx
@@ -16,6 +16,8 @@ export {
useGetMyIp,
useGetMyOrganizationProjects,
useGetMySessions,
+ useGetOrgMembership,
+ useGetOrgMembershipProjectMemberships,
useGetOrgUsers,
useGetUser,
useGetUserAction,
@@ -23,6 +25,5 @@ export {
useRegisterUserAction,
useRevokeMySessions,
useUpdateMfaEnabled,
- useUpdateOrgUserRole,
- useUpdateUserAuthMethods
-} from "./queries";
+ useUpdateOrgMembership,
+ useUpdateUserAuthMethods} from "./queries";
diff --git a/frontend/src/hooks/api/users/mutation.tsx b/frontend/src/hooks/api/users/mutation.tsx
index 26e932ac6..eb9bb8007 100644
--- a/frontend/src/hooks/api/users/mutation.tsx
+++ b/frontend/src/hooks/api/users/mutation.tsx
@@ -57,8 +57,9 @@ export const useAddUserToWsNonE2EE = () => {
});
return data;
},
- onSuccess: (_, { projectId }) => {
+ onSuccess: (_, { orgId, projectId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(projectId));
+ queryClient.invalidateQueries(userKeys.allOrgMembershipProjectMemberships(orgId));
}
});
};
diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx
index 5ca47fc63..ea3f44676 100644
--- a/frontend/src/hooks/api/users/queries.tsx
+++ b/frontend/src/hooks/api/users/queries.tsx
@@ -13,7 +13,8 @@ import {
OrgUser,
RenameUserDTO,
TokenVersion,
- UpdateOrgUserRoleDTO,
+ TWorkspaceUser,
+ UpdateOrgMembershipDTO,
User,
UserEnc
} from "./types";
@@ -23,6 +24,13 @@ export const userKeys = {
getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const,
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
+ getOrgMembership: (orgId: string, orgMembershipId: string) =>
+ [{ orgId, orgMembershipId }, "org-membership"] as const,
+ allOrgMembershipProjectMemberships: (orgId: string) => [orgId, "all-user-memberships"] as const,
+ forOrgMembershipProjectMemberships: (orgId: string, orgMembershipId: string) =>
+ [...userKeys.allOrgMembershipProjectMemberships(orgId), { orgMembershipId }] as const,
+ getOrgMembershipProjectMemberships: (orgId: string, username: string) =>
+ [{ orgId, username }, "org-membership-project-memberships"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
@@ -167,6 +175,41 @@ export const useAddUserToOrg = () => {
});
};
+export const useGetOrgMembership = (organizationId: string, orgMembershipId: string) => {
+ return useQuery({
+ queryKey: userKeys.getOrgMembership(organizationId, orgMembershipId),
+ queryFn: async () => {
+ const {
+ data: { membership }
+ } = await apiRequest.get<{ membership: OrgUser }>(
+ `/api/v2/organizations/${organizationId}/memberships/${orgMembershipId}`
+ );
+
+ return membership;
+ },
+ enabled: Boolean(organizationId) && Boolean(orgMembershipId)
+ });
+};
+
+export const useGetOrgMembershipProjectMemberships = (
+ organizationId: string,
+ orgMembershipId: string
+) => {
+ return useQuery({
+ queryKey: userKeys.forOrgMembershipProjectMemberships(organizationId, orgMembershipId),
+ queryFn: async () => {
+ const {
+ data: { memberships }
+ } = await apiRequest.get<{ memberships: TWorkspaceUser[] }>(
+ `/api/v2/organizations/${organizationId}/memberships/${orgMembershipId}/project-memberships`
+ );
+
+ return memberships;
+ },
+ enabled: Boolean(organizationId) && Boolean(orgMembershipId)
+ });
+};
+
export const useDeleteOrgMembership = () => {
const queryClient = useQueryClient();
@@ -180,24 +223,43 @@ export const useDeleteOrgMembership = () => {
});
};
-export const useUpdateOrgUserRole = () => {
+export const useDeactivateOrgMembership = () => {
const queryClient = useQueryClient();
- return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
- mutationFn: ({ organizationId, membershipId, role }) => {
+ return useMutation<{}, {}, DeletOrgMembershipDTO>({
+ mutationFn: ({ membershipId, orgId }) => {
+ return apiRequest.post(
+ `/api/v2/organizations/${orgId}/memberships/${membershipId}/deactivate`
+ );
+ },
+ onSuccess: (_, { orgId, membershipId }) => {
+ queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
+ queryClient.invalidateQueries(userKeys.getOrgMembership(orgId, membershipId));
+ }
+ });
+};
+
+export const useUpdateOrgMembership = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, UpdateOrgMembershipDTO>({
+ mutationFn: ({ organizationId, membershipId, role, isActive }) => {
return apiRequest.patch(
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
{
- role
+ role,
+ isActive
}
);
},
- onSuccess: (_, { organizationId }) => {
+ onSuccess: (_, { organizationId, membershipId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+ queryClient.invalidateQueries(userKeys.getOrgMembership(organizationId, membershipId));
},
// to remove old states
- onError: (_, { organizationId }) => {
+ onError: (_, { organizationId, membershipId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
+ queryClient.invalidateQueries(userKeys.getOrgMembership(organizationId, membershipId));
}
});
};
diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts
index a3793f942..4e2d7d3a2 100644
--- a/frontend/src/hooks/api/users/types.ts
+++ b/frontend/src/hooks/api/users/types.ts
@@ -49,6 +49,7 @@ export type OrgUser = {
user: {
username: string;
email?: string;
+ isEmailVerified: boolean;
firstName: string;
lastName: string;
id: string;
@@ -82,6 +83,11 @@ export type TWorkspaceUser = {
id: string;
publicKey: string;
};
+ projectId: string;
+ project: {
+ id: string;
+ name: string;
+ };
inviteEmail: string;
organization: string;
roles: (
@@ -127,12 +133,14 @@ export type AddUserToWsDTOE2EE = {
export type AddUserToWsDTONonE2EE = {
projectId: string;
usernames: string[];
+ orgId: string;
};
-export type UpdateOrgUserRoleDTO = {
+export type UpdateOrgMembershipDTO = {
organizationId: string;
membershipId: string;
- role: string;
+ role?: string;
+ isActive?: boolean;
};
export type DeletOrgMembershipDTO = {
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index 1b818fd83..11579b760 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -11,6 +11,7 @@ import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types";
import { EncryptedSecret } from "../secrets/types";
+import { userKeys } from "../users/queries";
import { TWorkspaceUser } from "../users/types";
import {
CreateEnvironmentDTO,
@@ -385,6 +386,7 @@ export const useDeleteUserFromWorkspace = () => {
}: {
workspaceId: string;
usernames: string[];
+ orgId: string;
}) => {
const {
data: { deletedMembership }
@@ -393,8 +395,9 @@ export const useDeleteUserFromWorkspace = () => {
});
return deletedMembership;
},
- onSuccess: (_, { workspaceId }) => {
+ onSuccess: (_, { orgId, workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
+ queryClient.invalidateQueries(userKeys.allOrgMembershipProjectMemberships(orgId));
}
});
};
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
index dc0ef370e..be4cf2086 100644
--- a/frontend/src/layouts/AppLayout/AppLayout.tsx
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -264,7 +264,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
usernames: orgUsers
.map((member) => member.user.username)
.filter((username) => username !== user.username),
- projectId: newProjectId
+ projectId: newProjectId,
+ orgId: currentOrg.id
});
}
diff --git a/frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx b/frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx
new file mode 100644
index 000000000..093b7cd78
--- /dev/null
+++ b/frontend/src/pages/org/[id]/memberships/[membershipId]/index.tsx
@@ -0,0 +1,20 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { useTranslation } from "react-i18next";
+import Head from "next/head";
+
+import { UserPage } from "@app/views/Org/UserPage";
+
+export default function User() {
+ const { t } = useTranslation();
+ return (
+ <>
+
+ {t("common.head-title", { title: t("settings.org.title") })}
+
+
+
+ >
+ );
+}
+
+User.requireAuth = true;
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx
index 0dc8d50a5..6956f71a3 100644
--- a/frontend/src/pages/org/[id]/overview/index.tsx
+++ b/frontend/src/pages/org/[id]/overview/index.tsx
@@ -541,7 +541,8 @@ const OrganizationPage = withPermission(
usernames: orgUsers
.map((member) => member.user.username)
.filter((username) => username !== user.username),
- projectId: newProjectId
+ projectId: newProjectId,
+ orgId: currentOrg.id
});
}
diff --git a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx
index 504c2405e..a005ad1c8 100644
--- a/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx
+++ b/frontend/src/views/Org/IdentityPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx
@@ -1,4 +1,4 @@
-import { faKey } from "@fortawesome/free-solid-svg-icons";
+import { faFolder } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
@@ -37,7 +37,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
- {isLoading && }
+ {isLoading && }
{!isLoading &&
projectMemberships?.map((membership) => {
return (
@@ -51,7 +51,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) =>
{!isLoading && !projectMemberships?.length && (
-
+
)}
);
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
index f9dc72f4e..e4a54f1b5 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
@@ -16,7 +16,7 @@ import {
useOrganization,
useSubscription
} from "@app/context";
-import { useDeleteOrgMembership } from "@app/hooks/api";
+import { useDeleteOrgMembership, useUpdateOrgMembership } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { AddOrgMemberModal } from "./AddOrgMemberModal";
@@ -32,11 +32,13 @@ export const OrgMembersSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addMember",
"removeMember",
+ "deactivateMember",
"upgradePlan",
"setUpEmail"
] as const);
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
+ const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const isMoreUsersAllowed = subscription?.memberLimit
? subscription.membersUsed < subscription.memberLimit
@@ -65,6 +67,29 @@ export const OrgMembersSection = () => {
handlePopUpOpen("addMember");
};
+ const onDeactivateMemberSubmit = async (orgMembershipId: string) => {
+ try {
+ await updateOrgMembership({
+ organizationId: orgId,
+ membershipId: orgMembershipId,
+ isActive: false
+ });
+
+ createNotification({
+ text: "Successfully deactivated user in organization",
+ type: "success"
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: "Failed to deactivate user in organization",
+ type: "error"
+ });
+ }
+
+ handlePopUpClose("deactivateMember");
+ };
+
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
try {
await deleteMutateAsync({
@@ -128,6 +153,19 @@ export const OrgMembersSection = () => {
)
}
/>
+ handlePopUpToggle("deactivateMember", isOpen)}
+ deleteKey="confirm"
+ onDeleteApproved={() =>
+ onDeactivateMemberSubmit(
+ (popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
+ )
+ }
+ />
handlePopUpToggle("upgradePlan", isOpen)}
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx
index c62b31592..ae75c5186 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx
@@ -1,13 +1,18 @@
import { useCallback, useMemo, useState } from "react";
-import { faMagnifyingGlass, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { useRouter } from "next/router";
+import { faEllipsis,faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
EmptyState,
- IconButton,
Input,
Select,
SelectItem,
@@ -32,13 +37,13 @@ import {
useFetchServerStatus,
useGetOrgRoles,
useGetOrgUsers,
- useUpdateOrgUserRole
+ useUpdateOrgMembership
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
- popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
+ popUpName: keyof UsePopUpState<["removeMember", "deactivateMember", "upgradePlan"]>,
data?: {
orgMembershipId?: string;
username?: string;
@@ -49,6 +54,7 @@ type Props = {
};
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
+ const router = useRouter();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { user } = useUser();
@@ -63,14 +69,14 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
- const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole();
+ const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const onRoleChange = async (membershipId: string, role: string) => {
if (!currentOrg?.id) return;
try {
// TODO: replace hardcoding default role
- const isCustomRole = !["admin", "member"].includes(role);
+ const isCustomRole = !["admin", "member", "no-access"].includes(role);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
@@ -79,7 +85,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
return;
}
- await updateUserOrgRole({
+ await updateOrgMembership({
organizationId: currentOrg?.id,
membershipId,
role
@@ -176,7 +182,11 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-";
return (
-
+
router.push(`/org/${orgId}/memberships/${orgMembershipId}`)}
+ >
| {name} |
{username} |
@@ -238,34 +248,117 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
|
{userId !== u?.id && (
-
- {(isAllowed) => (
- {
- if (currentOrg?.authEnforced) {
- createNotification({
- text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
- type: "error"
- });
- return;
- }
-
- handlePopUpOpen("removeMember", { orgMembershipId, username });
- }}
- size="lg"
- colorSchema="danger"
- variant="plain"
- ariaLabel="update"
- className="ml-4"
- isDisabled={!isAllowed}
+
+
+
+
+
+
+
+
-
-
- )}
-
+ {(isAllowed) => (
+ {
+ e.stopPropagation();
+ router.push(`/org/${orgId}/memberships/${orgMembershipId}`);
+ }}
+ disabled={!isAllowed}
+ >
+ Edit User
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ e.stopPropagation();
+
+ if (currentOrg?.authEnforced) {
+ createNotification({
+ text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
+ type: "error"
+ });
+ return;
+ }
+
+ if (!isActive) {
+ // activate user
+ await updateOrgMembership({
+ organizationId: orgId,
+ membershipId: orgMembershipId,
+ isActive: true
+ });
+
+ return;
+ }
+
+ // deactivate user
+ handlePopUpOpen("deactivateMember", {
+ orgMembershipId,
+ username
+ });
+ }}
+ disabled={!isAllowed}
+ >
+ {`${isActive ? "Deactivate" : "Activate"} User`}
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ e.stopPropagation();
+
+ if (currentOrg?.authEnforced) {
+ createNotification({
+ text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
+ type: "error"
+ });
+ return;
+ }
+
+ handlePopUpOpen("removeMember", {
+ orgMembershipId,
+ username
+ });
+ }}
+ disabled={!isAllowed}
+ >
+ Remove User
+
+ )}
+
+
+
)}
|
diff --git a/frontend/src/views/Org/UserPage/UserPage.tsx b/frontend/src/views/Org/UserPage/UserPage.tsx
new file mode 100644
index 000000000..7253d1c77
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/UserPage.tsx
@@ -0,0 +1,277 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { useRouter } from "next/router";
+import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+import { createNotification } from "@app/components/notifications";
+import { OrgPermissionCan } from "@app/components/permissions";
+import {
+ Button,
+ DeleteActionModal,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ Tooltip,
+ UpgradePlanModal
+} from "@app/components/v2";
+import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
+import { withPermission } from "@app/hoc";
+import {
+ useDeleteOrgMembership,
+ useGetOrgMembership,
+ useUpdateOrgMembership
+} from "@app/hooks/api";
+import { usePopUp } from "@app/hooks/usePopUp";
+
+import { UserDetailsSection, UserOrgMembershipModal,UserProjectsSection } from "./components";
+
+export const UserPage = withPermission(
+ () => {
+ const router = useRouter();
+ const membershipId = router.query.membershipId as string;
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+
+ const { data: membership } = useGetOrgMembership(orgId, membershipId);
+
+ const { mutateAsync: deleteOrgMembership } = useDeleteOrgMembership();
+ const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
+
+ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
+ "removeMember",
+ "orgMembership",
+ "deactivateMember",
+ "upgradePlan"
+ ] as const);
+
+ const onDeactivateMemberSubmit = async (orgMembershipId: string) => {
+ try {
+ await updateOrgMembership({
+ organizationId: orgId,
+ membershipId: orgMembershipId,
+ isActive: false
+ });
+
+ createNotification({
+ text: "Successfully deactivated user in organization",
+ type: "success"
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: "Failed to deactivate user in organization",
+ type: "error"
+ });
+ }
+
+ handlePopUpClose("deactivateMember");
+ };
+
+ const onRemoveMemberSubmit = async (orgMembershipId: string) => {
+ try {
+ await deleteOrgMembership({
+ orgId,
+ membershipId: orgMembershipId
+ });
+
+ createNotification({
+ text: "Successfully removed user from org",
+ type: "success"
+ });
+
+ handlePopUpClose("removeMember");
+ router.push(`/org/${orgId}/members`);
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: "Failed to remove user from the organization",
+ type: "error"
+ });
+ }
+
+ handlePopUpClose("removeMember");
+ };
+
+ return (
+
+ {membership && (
+
+
}
+ onClick={() => {
+ router.push(`/org/${orgId}/members`);
+ }}
+ className="mb-4"
+ >
+ Users
+
+
+
+ {membership.user.firstName || membership.user.lastName
+ ? `${membership.user.firstName} ${membership.user.lastName}`
+ : "-"}
+
+
+
+
+
+
+
+
+
+
+
+ {(isAllowed) => (
+
+ handlePopUpOpen("orgMembership", {
+ membershipId: membership.id,
+ role: membership.role
+ })
+ }
+ disabled={!isAllowed}
+ >
+ Edit User
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ if (currentOrg?.authEnforced) {
+ createNotification({
+ text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
+ type: "error"
+ });
+ return;
+ }
+
+ if (!membership.isActive) {
+ // activate user
+ await updateOrgMembership({
+ organizationId: orgId,
+ membershipId,
+ isActive: true
+ });
+
+ return;
+ }
+
+ // deactivate user
+ handlePopUpOpen("deactivateMember", {
+ orgMembershipId: membershipId,
+ username: membership.user.username
+ });
+ }}
+ disabled={!isAllowed}
+ >
+ {`${membership.isActive ? "Deactivate" : "Activate"} User`}
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ if (currentOrg?.authEnforced) {
+ createNotification({
+ text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
+ type: "error"
+ });
+ return;
+ }
+
+ handlePopUpOpen("removeMember", {
+ orgMembershipId: membershipId,
+ username: membership.user.username
+ });
+ }}
+ disabled={!isAllowed}
+ >
+ Remove User
+
+ )}
+
+
+
+
+
+
+ )}
+
handlePopUpToggle("removeMember", isOpen)}
+ deleteKey="confirm"
+ onDeleteApproved={() =>
+ onRemoveMemberSubmit(
+ (popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
+ )
+ }
+ />
+ handlePopUpToggle("deactivateMember", isOpen)}
+ deleteKey="confirm"
+ onDeleteApproved={() =>
+ onDeactivateMemberSubmit(
+ (popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
+ )
+ }
+ />
+ handlePopUpToggle("upgradePlan", isOpen)}
+ text={(popUp.upgradePlan?.data as { description: string })?.description}
+ />
+
+
+ );
+ },
+ { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }
+);
diff --git a/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx b/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx
new file mode 100644
index 000000000..00c605f05
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserDetailsSection.tsx
@@ -0,0 +1,195 @@
+import {
+ faCheck,
+ faCheckCircle,
+ faCircleXmark,
+ faCopy,
+ faPencil} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { createNotification } from "@app/components/notifications";
+import { OrgPermissionCan } from "@app/components/permissions";
+import { Button, IconButton, Tooltip } from "@app/components/v2";
+import {
+ OrgPermissionActions,
+ OrgPermissionSubjects,
+ useOrganization,
+ useUser
+} from "@app/context";
+import { useTimedReset } from "@app/hooks";
+import {
+ useAddUserToOrg,
+ useFetchServerStatus,
+ useGetOrgMembership,
+ useGetOrgRoles
+} from "@app/hooks/api";
+import { OrgUser } from "@app/hooks/api/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+type Props = {
+ membershipId: string;
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["orgMembership"]>, data?: {}) => void;
+};
+
+export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => {
+ const [copyTextUsername, isCopyingUsername, setCopyTextUsername] = useTimedReset({
+ initialState: "Copy username to clipboard"
+ });
+
+ const { user } = useUser();
+ const { currentOrg } = useOrganization();
+ const userId = user?.id || "";
+ const orgId = currentOrg?.id || "";
+
+ const { data: roles } = useGetOrgRoles(orgId);
+ const { data: serverDetails } = useFetchServerStatus();
+ const { data: membership } = useGetOrgMembership(orgId, membershipId);
+ const { mutateAsync: inviteUser, isLoading } = useAddUserToOrg();
+
+ const onResendInvite = async (email: string) => {
+ try {
+ const { data } = await inviteUser({
+ organizationId: orgId,
+ inviteeEmail: email
+ });
+
+ // setCompleteInviteLink(data?.completeInviteLink || "");
+
+ if (!data.completeInviteLink) {
+ createNotification({
+ text: `Successfully resent invite to ${email}`,
+ type: "success"
+ });
+ }
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: `Failed to resend invite to ${email}`,
+ type: "error"
+ });
+ }
+ };
+
+ const getStatus = (m: OrgUser) => {
+ if (!m.isActive) {
+ return "Deactivated";
+ }
+
+ return m.status === "invited" ? "Invited" : "Active";
+ };
+
+ const roleName = roles?.find((r) => r.slug === membership?.role)?.name;
+
+ return membership ? (
+
+
+
Details
+ {userId !== membership.user.id && (
+
+ {(isAllowed) => {
+ return (
+
+ {
+ handlePopUpOpen("orgMembership", {
+ membershipId: membership.id,
+ role: membership.role
+ });
+ }}
+ >
+
+
+
+ );
+ }}
+
+ )}
+
+
+
+
Name
+
+ {membership.user.firstName || membership.user.lastName
+ ? `${membership.user.firstName} ${membership.user.lastName}`
+ : "-"}
+
+
+
+
Username
+
+
{membership.user.username}
+
+
+ {
+ navigator.clipboard.writeText("");
+ setCopyTextUsername("Copied");
+ }}
+ >
+
+
+
+
+
+
+
+
Email
+
+
{membership.user.email ?? "-"}
+
+
+
+
+
+
+
Organization Role
+
{roleName ?? "-"}
+
+
+
Status
+
{getStatus(membership)}
+
+ {membership.isActive &&
+ (membership.status === "invited" || membership.status === "verified") &&
+ membership.user.email &&
+ serverDetails?.emailConfigured && (
+
+ {(isAllowed) => {
+ return (
+
+ );
+ }}
+
+ )}
+
+
+ ) : (
+
+ );
+};
diff --git a/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx b/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx
new file mode 100644
index 000000000..ce3db38d7
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserOrgMembershipModal.tsx
@@ -0,0 +1,160 @@
+import { useEffect } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
+import { useOrganization, useSubscription } from "@app/context";
+import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+const schema = z.object({
+ role: z.string()
+});
+
+export type FormData = z.infer;
+
+type Props = {
+ popUp: UsePopUpState<["orgMembership"]>;
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>, data?: {}) => void;
+ handlePopUpToggle: (popUpName: keyof UsePopUpState<["orgMembership"]>, state?: boolean) => void;
+};
+
+export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
+ const { subscription } = useSubscription();
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+
+ const { data: roles } = useGetOrgRoles(orgId);
+
+ const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(schema)
+ });
+
+ const popUpData = popUp?.orgMembership?.data as {
+ membershipId: string;
+ role: string;
+ };
+
+ useEffect(() => {
+ if (!roles?.length) return;
+
+ if (popUpData) {
+ reset({
+ role: popUpData.role
+ });
+ } else {
+ reset({
+ role: roles[0].slug
+ });
+ }
+ }, [popUp?.orgMembership?.data, roles]);
+
+ const onFormSubmit = async ({ role }: FormData) => {
+ try {
+ if (!orgId) return;
+
+ await updateOrgMembership({
+ organizationId: orgId,
+ membershipId: popUpData.membershipId,
+ role
+ });
+
+ handlePopUpToggle("orgMembership", false);
+
+ createNotification({
+ text: "Successfully updated user organization role",
+ type: "success"
+ });
+
+ reset();
+ } catch (err) {
+ console.error(err);
+ const error = err as any;
+ const text = error?.response?.data?.message ?? "Failed to update user organization role";
+
+ createNotification({
+ text,
+ type: "error"
+ });
+ }
+ };
+
+ return (
+ {
+ handlePopUpToggle("orgMembership", isOpen);
+ reset();
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx
new file mode 100644
index 000000000..2cef4f5f3
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx
@@ -0,0 +1,145 @@
+import { useMemo } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
+import { useOrganization, useWorkspace } from "@app/context";
+import { useAddUserToWsNonE2EE, useGetOrgMembershipProjectMemberships } from "@app/hooks/api";
+import { ProjectVersion } from "@app/hooks/api/workspace/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+const schema = z
+ .object({
+ projectId: z.string()
+ })
+ .required();
+
+type FormData = z.infer;
+
+type Props = {
+ membershipId: string;
+ popUp: UsePopUpState<["addUserToProject"]>;
+ handlePopUpToggle: (
+ popUpName: keyof UsePopUpState<["addUserToProject"]>,
+ state?: boolean
+ ) => void;
+};
+
+export const UserAddToProjectModal = ({ membershipId, popUp, handlePopUpToggle }: Props) => {
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+ const { workspaces } = useWorkspace();
+
+ const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE();
+
+ const popupData = popUp.addUserToProject.data as {
+ username: string;
+ };
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(schema)
+ });
+
+ const { data: projectMemberships } = useGetOrgMembershipProjectMemberships(orgId, membershipId);
+
+ const filteredWorkspaces = useMemo(() => {
+ const wsWorkspaceIds = new Map();
+
+ projectMemberships?.forEach((projectMembership) => {
+ wsWorkspaceIds.set(projectMembership.project.id, true);
+ });
+
+ return (workspaces || []).filter(
+ ({ id, orgId: projectOrgId, version }) =>
+ !wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version === ProjectVersion.V2
+ );
+ }, [workspaces, projectMemberships]);
+
+ const onFormSubmit = async ({ projectId }: FormData) => {
+ try {
+ await addUserToWorkspaceNonE2EE({
+ projectId,
+ usernames: [popupData.username],
+ orgId
+ });
+
+ createNotification({
+ text: "Successfully added user to project",
+ type: "success"
+ });
+
+ reset();
+ handlePopUpToggle("addUserToProject", false);
+ } catch (err) {
+ console.error(err);
+ const error = err as any;
+ const text = error?.response?.data?.message ?? "Failed to add identity to project";
+
+ createNotification({
+ text,
+ type: "error"
+ });
+ }
+ };
+
+ return (
+ {
+ handlePopUpToggle("addUserToProject", isOpen);
+ reset();
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectRow.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectRow.tsx
new file mode 100644
index 000000000..31c3184af
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectRow.tsx
@@ -0,0 +1,90 @@
+import { useMemo } from "react";
+import { useRouter } from "next/router";
+import { faTrash } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { createNotification } from "@app/components/notifications";
+import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
+import { useWorkspace } from "@app/context";
+import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
+import { TWorkspaceUser } from "@app/hooks/api/types";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+type Props = {
+ membership: TWorkspaceUser;
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromProject"]>, data?: {}) => void;
+};
+
+const formatRoleName = (role: string, customRoleName?: string) => {
+ if (role === ProjectMembershipRole.Custom) return customRoleName;
+ if (role === ProjectMembershipRole.Admin) return "Admin";
+ if (role === ProjectMembershipRole.Member) return "Developer";
+ if (role === ProjectMembershipRole.Viewer) return "Viewer";
+ if (role === ProjectMembershipRole.NoAccess) return "No Access";
+ return role;
+};
+
+export const UserProjectRow = ({
+ membership: { id, project, user, roles },
+ handlePopUpOpen
+}: Props) => {
+ const { workspaces } = useWorkspace();
+ const router = useRouter();
+
+ const isAccessible = useMemo(() => {
+ const workspaceIds = new Map();
+
+ workspaces?.forEach((workspace) => {
+ workspaceIds.set(workspace.id, true);
+ });
+
+ return workspaceIds.has(project.id);
+ }, [workspaces, project]);
+
+ return (
+ {
+ if (isAccessible) {
+ router.push(`/project/${project.id}/members`);
+ return;
+ }
+
+ createNotification({
+ text: "Unable to access project",
+ type: "error"
+ });
+ }}
+ >
+ | {project.name} |
+ {`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
+ roles.length > 1 ? ` (+${roles.length - 1})` : ""
+ }`} |
+
+ {isAccessible && (
+
+
+ {
+ e.stopPropagation();
+ handlePopUpOpen("removeUserFromProject", {
+ username: user.username,
+ projectId: project.id,
+ projectName: project.name
+ });
+ }}
+ >
+
+
+
+
+ )}
+ |
+
+ );
+};
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
new file mode 100644
index 000000000..ffd118abf
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
@@ -0,0 +1,98 @@
+import { faPlus } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { createNotification } from "@app/components/notifications";
+import { DeleteActionModal, IconButton } from "@app/components/v2";
+import { useOrganization,useUser } from "@app/context";
+import { useDeleteUserFromWorkspace, useGetOrgMembership } from "@app/hooks/api";
+import { usePopUp } from "@app/hooks/usePopUp";
+
+import { UserAddToProjectModal } from "./UserAddToProjectModal";
+import { UserProjectsTable } from "./UserProjectsTable";
+
+type Props = {
+ membershipId: string;
+};
+
+export const UserProjectsSection = ({ membershipId }: Props) => {
+ const { user } = useUser();
+ const { currentOrg } = useOrganization();
+
+ const userId = user?.id || "";
+ const orgId = currentOrg?.id || "";
+
+ const { data: membership } = useGetOrgMembership(orgId, membershipId);
+
+ const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
+
+ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
+ "addUserToProject",
+ "removeUserFromProject"
+ ] as const);
+
+ const handleRemoveUser = async (projectId: string, username: string) => {
+ try {
+ await removeUserFromWorkspace({ workspaceId: projectId, usernames: [username], orgId });
+ createNotification({
+ text: "Successfully removed user from project",
+ type: "success"
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to remove user from the project",
+ type: "error"
+ });
+ }
+ handlePopUpClose("removeUserFromProject");
+ };
+
+ return membership ? (
+
+
+
Projects
+ {userId !== membership.user.id && (
+ {
+ handlePopUpOpen("addUserToProject", {
+ username: membership.user.username
+ });
+ }}
+ >
+
+
+ )}
+
+
+
+
+
+
handlePopUpToggle("removeUserFromProject", isOpen)}
+ onDeleteApproved={() => {
+ const popupData = popUp?.removeUserFromProject?.data as {
+ username: string;
+ projectId: string;
+ projectName: string;
+ };
+
+ return handleRemoveUser(popupData.projectId, popupData.username);
+ }}
+ />
+
+ ) : (
+
+ );
+};
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsTable.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsTable.tsx
new file mode 100644
index 000000000..88e1a2925
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsTable.tsx
@@ -0,0 +1,62 @@
+import { faFolder } from "@fortawesome/free-solid-svg-icons";
+
+import {
+ EmptyState,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Th,
+ THead,
+ Tr
+} from "@app/components/v2";
+import { useOrganization } from "@app/context";
+import { useGetOrgMembershipProjectMemberships } from "@app/hooks/api";
+import { UsePopUpState } from "@app/hooks/usePopUp";
+
+import { UserProjectRow } from "./UserProjectRow";
+
+type Props = {
+ membershipId: string;
+ handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromProject"]>, data?: {}) => void;
+};
+
+export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => {
+ const { currentOrg } = useOrganization();
+ const orgId = currentOrg?.id || "";
+
+ const { data: projectMemberships, isLoading } = useGetOrgMembershipProjectMemberships(
+ orgId,
+ membershipId
+ );
+
+ return (
+
+
+
+
+ | Name |
+ Role |
+ |
+
+
+
+ {isLoading && }
+ {!isLoading &&
+ projectMemberships?.map((membership) => {
+ return (
+
+ );
+ })}
+
+
+ {!isLoading && !projectMemberships?.length && (
+
+ )}
+
+ );
+};
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/index.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/index.tsx
new file mode 100644
index 000000000..b298679d5
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/index.tsx
@@ -0,0 +1 @@
+export { UserProjectsSection } from "./UserProjectsSection";
diff --git a/frontend/src/views/Org/UserPage/components/index.tsx b/frontend/src/views/Org/UserPage/components/index.tsx
new file mode 100644
index 000000000..6b13786d4
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/components/index.tsx
@@ -0,0 +1,3 @@
+export { UserDetailsSection } from "./UserDetailsSection";
+export { UserOrgMembershipModal } from "./UserOrgMembershipModal";
+export { UserProjectsSection } from "./UserProjectsSection";
diff --git a/frontend/src/views/Org/UserPage/index.tsx b/frontend/src/views/Org/UserPage/index.tsx
new file mode 100644
index 000000000..3338fc5e4
--- /dev/null
+++ b/frontend/src/views/Org/UserPage/index.tsx
@@ -0,0 +1 @@
+export { UserPage } from "./UserPage";
diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx
index df76520c2..cd8137d8b 100644
--- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx
+++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx
@@ -6,14 +6,15 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
-import { Button,FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
+import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useGetOrgUsers,
useGetUserWsKey,
- useGetWorkspaceUsers} from "@app/hooks/api";
+ useGetWorkspaceUsers
+} from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -76,7 +77,8 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
} else if (currentWorkspace.version === ProjectVersion.V2) {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
- usernames: [orgUser.user.username]
+ usernames: [orgUser.user.username],
+ orgId
});
} else {
createNotification({
From 6818c8730f0ed59434368386f4983ed4311a5429 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 16:49:54 +0200
Subject: [PATCH 03/30] chore: rename approverUserIds to approvers in
registerSecretApprovalPolicy
---
.../ee/routes/v1/secret-approval-policy-router.ts | 8 ++++----
.../secret-approval-policy-service.ts | 12 ++++++------
.../secret-approval-policy-types.ts | 4 ++--
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
index ee10131dd..f25ae42f3 100644
--- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
@@ -25,10 +25,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.optional()
.nullable()
.transform((val) => (val ? removeTrailingSlash(val) : val)),
- approverUserIds: z.string().array().min(1),
+ approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1)
})
- .refine((data) => data.approvals <= data.approverUserIds.length, {
+ .refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@@ -66,7 +66,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
body: z
.object({
name: z.string().optional(),
- approverUserIds: z.string().array().min(1),
+ approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
secretPath: z
.string()
@@ -74,7 +74,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.nullable()
.transform((val) => (val ? removeTrailingSlash(val) : val))
})
- .refine((data) => data.approvals <= data.approverUserIds.length, {
+ .refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
index 2db825c88..be6a334e2 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
@@ -45,12 +45,12 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId,
actorAuthMethod,
approvals,
- approverUserIds,
+ approvers,
projectId,
secretPath,
environment
}: TCreateSapDTO) => {
- if (approvals > approverUserIds.length)
+ if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
@@ -78,7 +78,7 @@ export const secretApprovalPolicyServiceFactory = ({
tx
);
await secretApprovalPolicyApproverDAL.insertMany(
- approverUserIds.map((approverUserId) => ({
+ approvers.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
@@ -90,7 +90,7 @@ export const secretApprovalPolicyServiceFactory = ({
};
const updateSecretApprovalPolicy = async ({
- approverUserIds,
+ approvers,
secretPath,
name,
actorId,
@@ -122,10 +122,10 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
- if (approverUserIds) {
+ if (approvers) {
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await secretApprovalPolicyApproverDAL.insertMany(
- approverUserIds.map((approverUserId) => ({
+ approvers.map((approverUserId) => ({
approverUserId,
policyId: doc.id
})),
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
index 1a527289c..2ddd9b51b 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
@@ -4,7 +4,7 @@ export type TCreateSapDTO = {
approvals: number;
secretPath?: string | null;
environment: string;
- approverUserIds: string[];
+ approvers: string[];
projectId: string;
name: string;
} & Omit;
@@ -13,7 +13,7 @@ export type TUpdateSapDTO = {
secretPolicyId: string;
approvals?: number;
secretPath?: string | null;
- approverUserIds: string[];
+ approvers: string[];
name?: string;
} & Omit;
From ada0033bd026e46bd1ee83228aa95f59d11e48f0 Mon Sep 17 00:00:00 2001
From: Tuan Dang
Date: Wed, 17 Jul 2024 23:13:04 +0700
Subject: [PATCH 04/30] Fix type issue frontend
---
.../MembersTab/components/MembersSection.tsx | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx
index b4c230e19..db69fc6e6 100644
--- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx
+++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersSection.tsx
@@ -4,7 +4,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub , useOrganization, useWorkspace } from "@app/context";
+import {
+ ProjectPermissionActions,
+ ProjectPermissionSub,
+ useOrganization,
+ useWorkspace
+} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteUserFromWorkspace } from "@app/hooks/api";
@@ -30,7 +35,11 @@ export const MembersSection = () => {
if (!currentWorkspace?.id) return;
try {
- await removeUserFromWorkspace({ workspaceId: currentWorkspace.id, usernames: [username] });
+ await removeUserFromWorkspace({
+ workspaceId: currentWorkspace.id,
+ usernames: [username],
+ orgId: currentOrg.id
+ });
createNotification({
text: "Successfully removed user from project",
type: "success"
From d19c856e9b4f917dcf37e67c1f8335195bdb0acb Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 20:05:34 +0200
Subject: [PATCH 05/30] chore(frontend): rename approverUserIds to approvers in
registerSecretApprovalPolicy
---
frontend/src/hooks/api/secretApproval/mutation.tsx | 8 ++++----
frontend/src/hooks/api/secretApproval/types.ts | 5 +++--
.../components/SecretApprovalPolicyRow.tsx | 2 +-
.../components/SecretPolicyForm.tsx | 8 ++++----
4 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx
index 991111ef9..e0a8df95d 100644
--- a/frontend/src/hooks/api/secretApproval/mutation.tsx
+++ b/frontend/src/hooks/api/secretApproval/mutation.tsx
@@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
- mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => {
+ mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
workspaceId,
approvals,
- approverUserIds,
+ approvers,
secretPath,
name
});
@@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
- mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => {
+ mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
- approverUserIds,
+ approvers,
secretPath,
name
});
diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts
index f3b8639f7..bc0d57d40 100644
--- a/frontend/src/hooks/api/secretApproval/types.ts
+++ b/frontend/src/hooks/api/secretApproval/types.ts
@@ -9,6 +9,7 @@ export type TSecretApprovalPolicy = {
secretPath?: string;
approvals: number;
userApprovers: { userId: string }[];
+ updatedAt: Date;
};
export type TGetSecretApprovalPoliciesDTO = {
@@ -26,14 +27,14 @@ export type TCreateSecretPolicyDTO = {
name?: string;
environment: string;
secretPath?: string | null;
- approverUserIds?: string[];
+ approvers?: string[];
approvals?: number;
};
export type TUpdateSecretPolicyDTO = {
id: string;
name?: string;
- approverUserIds?: string[];
+ approvers?: string[];
secretPath?: string | null;
approvals?: number;
// for invalidating list
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx
index d3321850a..9f8a08c44 100644
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx
@@ -51,7 +51,7 @@ export const SecretApprovalPolicyRow = ({
{
workspaceId,
id: policy.id,
- approverUserIds: selectedApprovers
+ approvers: selectedApprovers
},
{
onSettled: () => {
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx
index b0db2affd..18b0b87c8 100644
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx
@@ -37,9 +37,9 @@ const formSchema = z
name: z.string().optional(),
secretPath: z.string().optional().nullable(),
approvals: z.number().min(1),
- approverUserIds: z.string().array().min(1)
+ approvers: z.string().array().min(1)
})
- .refine((data) => data.approvals <= data.approverUserIds.length, {
+ .refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
});
@@ -62,7 +62,7 @@ export const SecretPolicyForm = ({
values: editValues
? {
...editValues,
- approverUserIds: editValues.userApprovers.map(({ userId }) => userId),
+ approvers: editValues.userApprovers.map(({ userId }) => userId),
environment: editValues.environment.slug
}
: undefined
@@ -183,7 +183,7 @@ export const SecretPolicyForm = ({
/>
(
Date: Wed, 17 Jul 2024 20:22:43 +0200
Subject: [PATCH 06/30] feat(frontend): welcome ApprovalPolicyList
---
frontend/src/helpers/policies.ts | 12 +
.../src/hooks/api/accessApproval/types.ts | 6 +
frontend/src/hooks/api/policies/enums.ts | 9 +
.../SecretApprovalPage/SecretApprovalPage.tsx | 28 +-
.../ApprovalPolicyList/ApprovalPolicyList.tsx | 262 ++++++++++++
.../components/AccessPolicyModal.tsx | 376 ++++++++++++++++++
.../components/ApprovalPolicyRow.tsx | 191 +++++++++
.../components/ApprovalPolicyList/index.tsx | 1 +
8 files changed, 881 insertions(+), 4 deletions(-)
create mode 100644 frontend/src/helpers/policies.ts
create mode 100644 frontend/src/hooks/api/policies/enums.ts
create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx
create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx
diff --git a/frontend/src/helpers/policies.ts b/frontend/src/helpers/policies.ts
new file mode 100644
index 000000000..c6d7c935a
--- /dev/null
+++ b/frontend/src/helpers/policies.ts
@@ -0,0 +1,12 @@
+import { PolicyType } from "@app/hooks/api/policies/enums";
+
+export const policyDetails: Record = {
+ [PolicyType.AccessPolicy]: {
+ className: "bg-lime-900 text-lime-100",
+ name: "Access Policy"
+ },
+ [PolicyType.ChangePolicy]: {
+ className: "bg-indigo-900 text-indigo-100",
+ name: "Change Policy"
+ }
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts
index 2176b8bc1..a6d09a227 100644
--- a/frontend/src/hooks/api/accessApproval/types.ts
+++ b/frontend/src/hooks/api/accessApproval/types.ts
@@ -1,3 +1,4 @@
+import { EnforcementLevel, PolicyType } from "../policies/enums";
import { TProjectPermission } from "../roles/types";
import { WorkspaceEnv } from "../workspace/types";
@@ -11,6 +12,11 @@ export type TAccessApprovalPolicy = {
environment: WorkspaceEnv;
projectId: string;
approvers: string[];
+ policyType: PolicyType;
+ approversRequired: boolean;
+ enforcementLevel: EnforcementLevel;
+ updatedAt: Date;
+ userApprovers?: { userId: string }[];
};
export type TAccessApprovalRequest = {
diff --git a/frontend/src/hooks/api/policies/enums.ts b/frontend/src/hooks/api/policies/enums.ts
new file mode 100644
index 000000000..f91bcb98c
--- /dev/null
+++ b/frontend/src/hooks/api/policies/enums.ts
@@ -0,0 +1,9 @@
+export enum EnforcementLevel {
+ Hard = "hard",
+ Soft = "soft"
+}
+
+export enum PolicyType {
+ ChangePolicy = "change",
+ AccessPolicy = "access"
+}
diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
index 9c273848f..f8d056cce 100644
--- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
+++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
@@ -3,11 +3,14 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
+import { Badge } from "@app/components/v2/Badge";
import { Divider } from "@app/components/v2/Divider";
import { useWorkspace } from "@app/context";
+import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList";
import { AccessApprovalRequest } from "./components/AccessApprovalRequest";
+import { ApprovalPolicyList } from "./components/ApprovalPolicyList";
import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList";
import { SecretApprovalRequest } from "./components/SecretApprovalRequest";
@@ -15,13 +18,19 @@ enum TabSection {
SecretApprovalRequests = "approval-requests",
SecretPolicies = "approval-rules",
ResourcePolicies = "resource-rules",
- ResourceApprovalRequests = "resource-requests"
+ ResourceApprovalRequests = "resource-requests",
+ Policies = "policies"
}
export const SecretApprovalPage = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
+ const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId: projectId });
+ const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
+ const defaultTab = (accessApprovalRequestCount?.pendingCount || 0) > (secretApprovalReqCount?.open || 0)
+ ? TabSection.ResourceApprovalRequests
+ : TabSection.SecretApprovalRequests;
return (
@@ -45,13 +54,21 @@ export const SecretApprovalPage = () => {
-
+
- Secret Requests
+
+ Secret Requests
+ {Boolean(secretApprovalReqCount?.open) && ({secretApprovalReqCount?.open})}
+
Secret Policies
- Access Requests
+
+ Access Requests
+ {Boolean(accessApprovalRequestCount?.pendingCount) && {accessApprovalRequestCount?.pendingCount}}
+
Access Request Policies
+
+ Policies
@@ -65,6 +82,9 @@ export const SecretApprovalPage = () => {
+
+
+
);
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx
new file mode 100644
index 000000000..fb21521c9
--- /dev/null
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx
@@ -0,0 +1,262 @@
+import { useMemo,useState } from "react";
+import { faCheckCircle,faChevronDown, faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { createNotification } from "@app/components/notifications";
+import { ProjectPermissionCan } from "@app/components/permissions";
+import {
+ Button,
+ DeleteActionModal,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+ EmptyState,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr,
+ UpgradePlanModal
+} from "@app/components/v2";
+import {
+ ProjectPermissionActions,
+ ProjectPermissionSub,
+ TProjectPermission,
+ useProjectPermission,
+ useSubscription,
+ useWorkspace
+} from "@app/context";
+import { usePopUp } from "@app/hooks";
+import { useDeleteAccessApprovalPolicy, useDeleteSecretApprovalPolicy, useGetSecretApprovalPolicies, useGetWorkspaceUsers } from "@app/hooks/api";
+import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
+import { PolicyType } from "@app/hooks/api/policies/enums";
+import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
+
+import { AccessPolicyForm } from "./components/AccessPolicyModal";
+import { ApprovalPolicyRow } from "./components/ApprovalPolicyRow";
+
+interface IProps {
+ workspaceId: string;
+}
+
+const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
+ const { data: accessPolicies, isLoading: isAccessPoliciesLoading } = useGetAccessApprovalPolicies({
+ projectSlug: currentWorkspace?.slug as string,
+ options: {
+ enabled:
+ permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
+ !!currentWorkspace?.slug
+ }
+ });
+ const { data: secretPolicies, isLoading: isSecretPoliciesLoading } = useGetSecretApprovalPolicies({
+ workspaceId: currentWorkspace?.id as string,
+ options: {
+ enabled:
+ permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
+ !!currentWorkspace?.id
+ }
+ });
+
+ // merge data sorted by updatedAt
+ const policies = [
+ ...(accessPolicies?.map(policy => ({ ...policy, policyType: PolicyType.AccessPolicy })) || []),
+ ...(secretPolicies?.map(policy => ({ ...policy, policyType: PolicyType.ChangePolicy })) || [])
+ ].sort((a, b) => {
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
+ });
+
+ return {
+ policies,
+ isLoading: isAccessPoliciesLoading || isSecretPoliciesLoading
+ };
+};
+
+export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
+ const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
+ "policyForm",
+ "deletePolicy",
+ "upgradePlan"
+ ] as const);
+ const { permission } = useProjectPermission();
+ const { subscription } = useSubscription();
+ const { currentWorkspace } = useWorkspace();
+
+ const { data: members } = useGetWorkspaceUsers(workspaceId);
+ const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(permission, currentWorkspace);
+
+ const [filterType, setFilterType] = useState(null);
+
+ const filteredPolicies = useMemo(() => {
+ return filterType
+ ? policies.filter(policy => policy.policyType === filterType)
+ : policies;
+ }, [policies, filterType]);
+
+ const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
+ const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
+
+ const handleDeletePolicy = async () => {
+ const { id, policyType } = popUp.deletePolicy.data as TAccessApprovalPolicy;
+ if (!currentWorkspace?.slug) return;
+
+ try {
+ if (policyType === PolicyType.ChangePolicy) {
+ await deleteSecretApprovalPolicy({
+ workspaceId,
+ id
+ });
+ } else {
+ await deleteAccessApprovalPolicy({
+ projectSlug: currentWorkspace?.slug,
+ id
+ });
+ }
+ createNotification({
+ type: "success",
+ text: "Successfully deleted policy"
+ });
+ handlePopUpClose("deletePolicy");
+ } catch (err) {
+ console.log(err);
+ createNotification({
+ type: "error",
+ text: "Failed to delete policy"
+ });
+ }
+ };
+
+ return (
+
+
+
+
Policies
+
+ Implement granular policies for access requests and secrets management.
+
+
+
+
+ {(isAllowed) => (
+
+ )}
+
+
+
+
+
+
+
+ | Name |
+ Environment |
+ Secret Path |
+ Eligible Approvers |
+ Approval Required |
+
+
+
+ }
+ >
+ Type
+
+
+
+ Select a type
+ setFilterType(null)}
+ icon={!filterType && }
+ iconPos="right"
+ >
+ All
+
+ setFilterType(PolicyType.AccessPolicy)}
+ icon={filterType === PolicyType.AccessPolicy && }
+ iconPos="right"
+ >
+ Access Policy
+
+ setFilterType(PolicyType.ChangePolicy)}
+ icon={filterType === PolicyType.ChangePolicy && }
+ iconPos="right"
+ >
+ Change Policy
+
+
+
+ |
+ |
+
+
+
+ {isPoliciesLoading && (
+
+ )}
+ {!isPoliciesLoading && !filteredPolicies?.length && (
+
+ |
+
+ |
+
+ )}
+ {!!currentWorkspace &&
+ filteredPolicies?.map((policy) => (
+ handlePopUpOpen("policyForm", policy)}
+ onDelete={() => handlePopUpOpen("deletePolicy", policy)}
+ />
+ ))}
+
+
+
+
handlePopUpToggle("policyForm", isOpen)}
+ members={members}
+ editValues={popUp.policyForm.data as TAccessApprovalPolicy}
+ />
+ handlePopUpToggle("deletePolicy", isOpen)}
+ onDeleteApproved={handleDeletePolicy}
+ />
+ handlePopUpToggle("upgradePlan", isOpen)}
+ text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
+ />
+
+ );
+};
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
new file mode 100644
index 000000000..9ebd36661
--- /dev/null
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
@@ -0,0 +1,376 @@
+import { useEffect } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Alert,
+ AlertDescription,
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+ FormControl,
+ Input,
+ Modal,
+ ModalContent,
+ Select,
+ SelectItem
+} from "@app/components/v2";
+import { useWorkspace } from "@app/context";
+import { policyDetails } from "@app/helpers/policies";
+import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
+import {
+ useCreateAccessApprovalPolicy,
+ useUpdateAccessApprovalPolicy
+} from "@app/hooks/api/accessApproval";
+import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
+import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
+import { TWorkspaceUser } from "@app/hooks/api/users/types";
+
+type Props = {
+ isOpen?: boolean;
+ onToggle: (isOpen: boolean) => void;
+ members?: TWorkspaceUser[];
+ projectSlug: string;
+ editValues?: TAccessApprovalPolicy;
+};
+
+const formSchema = z
+.object({
+ environment: z.string(),
+ name: z.string().optional(),
+ secretPath: z.string().optional(),
+ approvals: z.number().min(1),
+ approvers: z.string().array().min(1),
+ policyType: z.nativeEnum(PolicyType),
+ enforcementLevel: z.nativeEnum(EnforcementLevel)
+})
+.refine((data) => data.approvals <= data.approvers.length, {
+ path: ["approvals"],
+ message: "The number of approvals should be lower than the number of approvers."
+});
+
+type TFormSchema = z.infer;
+
+export const AccessPolicyForm = ({
+ isOpen,
+ onToggle,
+ members = [],
+ projectSlug,
+ editValues
+}: Props) => {
+ const {
+ control,
+ handleSubmit,
+ reset,
+ watch,
+ formState: { isSubmitting }
+ } = useForm({
+ resolver: zodResolver(formSchema),
+ values: editValues ? {
+ ...editValues,
+ environment: editValues.environment.slug,
+ approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
+ } : undefined
+ });
+ const { currentWorkspace } = useWorkspace();
+
+ const environments = currentWorkspace?.environments || [];
+ const isEditMode = Boolean(editValues);
+
+ useEffect(() => {
+ if (!isOpen || !isEditMode) reset({});
+ }, [isOpen, isEditMode]);
+
+ const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
+ const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
+
+ const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
+ const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
+
+ const enforcementLevel = watch("enforcementLevel");
+ const policyName = policyDetails[watch("policyType")]?.name || "Policy";
+
+ const handleCreatePolicy = async (data: TFormSchema) => {
+ if (!projectSlug) return;
+
+ try {
+ if (data.policyType === PolicyType.ChangePolicy) {
+ await createSecretApprovalPolicy({
+ ...data,
+ workspaceId: currentWorkspace?.id || ""
+ });
+ } else {
+ await createAccessApprovalPolicy({
+ ...data,
+ projectSlug
+ });
+ }
+ createNotification({
+ type: "success",
+ text: "Successfully created policy"
+ });
+ onToggle(false);
+ } catch (err) {
+ console.log(err);
+ createNotification({
+ type: "error",
+ text: "Failed to create policy"
+ });
+ }
+ };
+
+ const handleUpdatePolicy = async (data: TFormSchema) => {
+ if (!projectSlug) return;
+ if (!editValues?.id) return;
+
+ try {
+ if (data.policyType === PolicyType.ChangePolicy) {
+ await updateSecretApprovalPolicy({
+ id: editValues?.id,
+ ...data,
+ workspaceId: currentWorkspace?.id || ""
+ });
+ } else {
+ await updateAccessApprovalPolicy({
+ id: editValues?.id,
+ ...data,
+ projectSlug
+ });
+ createNotification({
+ type: "success",
+ text: "Successfully updated policy"
+ });
+ onToggle(false);
+ }
+ } catch (err) {
+ console.log(err);
+ createNotification({
+ type: "error",
+ text: "failed to update policy"
+ });
+ }
+ };
+
+ const handleFormSubmit = async (data: TFormSchema) => {
+ if (isEditMode) {
+ await handleUpdatePolicy(data);
+ } else {
+ await handleCreatePolicy(data);
+ }
+ };
+
+ const formatEnforcementLevel = (level: EnforcementLevel) => {
+ if (level === EnforcementLevel.Hard) return "Hard";
+ if (level === EnforcementLevel.Soft) return "Soft";
+ return level;
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
new file mode 100644
index 000000000..e251f88ca
--- /dev/null
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
@@ -0,0 +1,191 @@
+import { useState } from "react";
+import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { ProjectPermissionCan } from "@app/components/permissions";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+ IconButton,
+ Input,
+ Td,
+ Tr
+} from "@app/components/v2";
+import { Badge } from "@app/components/v2/Badge";
+import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
+import { policyDetails } from "@app/helpers/policies";
+import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
+import { PolicyType } from "@app/hooks/api/policies/enums";
+import { WorkspaceEnv } from "@app/hooks/api/types";
+import { TWorkspaceUser } from "@app/hooks/api/users/types";
+
+interface IPolicy {
+ id: string;
+ name: string;
+ environment: WorkspaceEnv;
+ projectId?: string;
+ secretPath?: string;
+ approvals: number;
+ approvers?: string[];
+ userApprovers?: { userId: string }[];
+ updatedAt: Date;
+ policyType: PolicyType;
+};
+
+type Props = {
+ policy: IPolicy;
+ members?: TWorkspaceUser[];
+ projectSlug: string;
+ workspaceId: string;
+ onEdit: () => void;
+ onDelete: () => void;
+};
+
+export const ApprovalPolicyRow = ({
+ policy,
+ members = [],
+ projectSlug,
+ workspaceId,
+ onEdit,
+ onDelete
+}: Props) => {
+ const [selectedApprovers, setSelectedApprovers] = useState(policy.userApprovers?.map(({ userId }) => userId) || policy.approvers || []);
+ const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } = useUpdateAccessApprovalPolicy();
+ const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } = useUpdateSecretApprovalPolicy();
+ const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading;
+
+ const { permission } = useProjectPermission();
+
+ return (
+
+ | {policy.name} |
+ {policy.environment.slug} |
+ {policy.secretPath || "*"} |
+
+ {
+ if (!isOpen) {
+ if (policy.policyType === PolicyType.AccessPolicy) {
+ updateAccessApprovalPolicy(
+ {
+ projectSlug,
+ id: policy.id,
+ approvers: selectedApprovers
+ },
+ {
+ onSettled: () => {
+ // No changes needed here
+ }
+ }
+ );
+ } else {
+ updateSecretApprovalPolicy(
+ {
+ workspaceId,
+ id: policy.id,
+ approvers: selectedApprovers
+ },
+ {
+ onSettled: () => {
+ // No changes needed here
+ }
+ }
+ );
+ }
+ } else {
+ setSelectedApprovers(policy.policyType === PolicyType.ChangePolicy
+ ? policy?.userApprovers?.map(({ userId }) => userId) || []
+ : policy?.approvers || []
+ );
+ }
+ }}
+ >
+
+
+
+
+
+ Select members that are allowed to approve changes
+
+ {members?.map(({ id, user }) => {
+ const userId = policy.policyType === PolicyType.ChangePolicy ? user.id : id;
+ const isChecked = selectedApprovers.includes(userId);
+ return (
+ {
+ evt.preventDefault();
+ setSelectedApprovers((state) =>
+ isChecked ? state.filter((el) => el !== userId) : [...state, userId]
+ );
+ }}
+ key={`create-policy-members-${userId}`}
+ iconPos="right"
+ icon={isChecked && }
+ >
+ {user.username}
+
+ );
+ })}
+
+
+ |
+ {policy.approvals} |
+
+
+ {policyDetails[policy.policyType].name}
+
+ |
+
+
+
+ {(isAllowed) => (
+
+
+
+ )}
+
+
+ {(isAllowed) => (
+
+
+
+ )}
+
+
+ |
+
+ );
+};
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx
new file mode 100644
index 000000000..2817ec627
--- /dev/null
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx
@@ -0,0 +1 @@
+export { ApprovalPolicyList } from "./ApprovalPolicyList";
From 7b3e116bf8f14bd2996b9881977a47025723c68f Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 20:26:27 +0200
Subject: [PATCH 07/30] feat(frontend): remove AccessApprovalPolicyList
---
.../SecretApprovalPage/SecretApprovalPage.tsx | 6 -
.../AccessApprovalPolicyList.tsx | 174 ------------
.../components/AccessApprovalPolicyRow.tsx | 146 ----------
.../components/AccessPolicyModal.tsx | 266 ------------------
.../AccessApprovalPolicyList/index.tsx | 1 -
5 files changed, 593 deletions(-)
delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx
delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx
delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx
delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx
diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
index f8d056cce..df0ba55f9 100644
--- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
+++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
@@ -8,7 +8,6 @@ import { Divider } from "@app/components/v2/Divider";
import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
-import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList";
import { AccessApprovalRequest } from "./components/AccessApprovalRequest";
import { ApprovalPolicyList } from "./components/ApprovalPolicyList";
import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList";
@@ -66,8 +65,6 @@ export const SecretApprovalPage = () => {
Access Requests
{Boolean(accessApprovalRequestCount?.pendingCount) && {accessApprovalRequestCount?.pendingCount}}
- Access Request Policies
-
Policies
@@ -79,9 +76,6 @@ export const SecretApprovalPage = () => {
-
-
-
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx
deleted file mode 100644
index aa47cce80..000000000
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { createNotification } from "@app/components/notifications";
-import { ProjectPermissionCan } from "@app/components/permissions";
-import {
- Button,
- DeleteActionModal,
- EmptyState,
- Table,
- TableContainer,
- TableSkeleton,
- TBody,
- Td,
- Th,
- THead,
- Tr,
- UpgradePlanModal
-} from "@app/components/v2";
-import {
- ProjectPermissionActions,
- ProjectPermissionSub,
- useProjectPermission,
- useSubscription,
- useWorkspace
-} from "@app/context";
-import { usePopUp } from "@app/hooks";
-import { useDeleteAccessApprovalPolicy, useGetWorkspaceUsers } from "@app/hooks/api";
-import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
-import { TAccessApprovalPolicy } from "@app/hooks/api/types";
-
-import { AccessApprovalPolicyRow } from "./components/AccessApprovalPolicyRow";
-import { AccessPolicyForm } from "./components/AccessPolicyModal";
-
-interface IProps {
- workspaceId: string;
-}
-
-export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => {
- const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
- "secretPolicyForm",
- "deletePolicy",
- "upgradePlan"
- ] as const);
- const { permission } = useProjectPermission();
- const { subscription } = useSubscription();
- const { currentWorkspace } = useWorkspace();
-
- const { data: members } = useGetWorkspaceUsers(workspaceId);
- const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({
- projectSlug: currentWorkspace?.slug as string,
- options: {
- enabled:
- permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
- !!currentWorkspace?.slug
- }
- });
-
- const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteAccessApprovalPolicy();
-
- const handleDeletePolicy = async () => {
- const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy;
- if (!currentWorkspace?.slug) return;
-
- try {
- await deleteSecretApprovalPolicy({
- projectSlug: currentWorkspace?.slug,
- id
- });
- createNotification({
- type: "success",
- text: "Successfully deleted policy"
- });
- handlePopUpClose("deletePolicy");
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "Failed to delete policy"
- });
- }
- };
-
- return (
-
-
-
-
Access Request Policies
-
- Implement secret request policies for specific secrets and environments.
-
-
-
-
- {(isAllowed) => (
-
- )}
-
-
-
-
-
-
-
- | Name |
- Environment |
- Secret Path |
- Eligible Approvers |
- Approval Required |
- |
-
-
-
- {isPoliciesLoading && (
-
- )}
- {!isPoliciesLoading && !policies?.length && (
-
- |
-
- |
-
- )}
- {!!currentWorkspace &&
- policies?.map((policy) => (
- handlePopUpOpen("secretPolicyForm", policy)}
- onDelete={() => handlePopUpOpen("deletePolicy", policy)}
- />
- ))}
-
-
-
-
handlePopUpToggle("secretPolicyForm", isOpen)}
- members={members}
- editValues={popUp.secretPolicyForm.data as TAccessApprovalPolicy}
- />
- handlePopUpToggle("deletePolicy", isOpen)}
- onDeleteApproved={handleDeletePolicy}
- />
- handlePopUpToggle("upgradePlan", isOpen)}
- text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
- />
-
- );
-};
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx
deleted file mode 100644
index 8476bac8d..000000000
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import { useState } from "react";
-import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { ProjectPermissionCan } from "@app/components/permissions";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
- IconButton,
- Input,
- Td,
- Tr
-} from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
-import { useUpdateAccessApprovalPolicy } from "@app/hooks/api";
-import { TAccessApprovalPolicy } from "@app/hooks/api/types";
-import { TWorkspaceUser } from "@app/hooks/api/users/types";
-
-type Props = {
- policy: TAccessApprovalPolicy;
- members?: TWorkspaceUser[];
- projectSlug: string;
- onEdit: () => void;
- onDelete: () => void;
-};
-
-export const AccessApprovalPolicyRow = ({
- policy,
- members = [],
- projectSlug,
- onEdit,
- onDelete
-}: Props) => {
- const [selectedApprovers, setSelectedApprovers] = useState([]);
- const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy();
- const { permission } = useProjectPermission();
-
- return (
-
- | {policy.name} |
- {policy.environment.slug} |
- {policy.secretPath || "*"} |
-
- {
- if (!isOpen) {
- updateAccessApprovalPolicy(
- {
- projectSlug,
- id: policy.id,
- approvers: selectedApprovers
- },
- {
- onSettled: () => {
- setSelectedApprovers([]);
- }
- }
- );
- } else {
- setSelectedApprovers(policy.approvers);
- }
- }}
- >
-
-
-
-
-
- Select members that are allowed to approve changes
-
- {members?.map(({ id, user }) => {
- const isChecked = selectedApprovers.includes(id);
- return (
- {
- evt.preventDefault();
- setSelectedApprovers((state) =>
- isChecked ? state.filter((el) => el !== id) : [...state, id]
- );
- }}
- key={`create-policy-members-${id}`}
- iconPos="right"
- icon={isChecked && }
- >
- {user.username}
-
- );
- })}
-
-
- |
- {policy.approvals} |
-
-
-
- {(isAllowed) => (
-
-
-
- )}
-
-
- {(isAllowed) => (
-
-
-
- )}
-
-
- |
-
- );
-};
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx
deleted file mode 100644
index 6c0ee3fb6..000000000
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx
+++ /dev/null
@@ -1,266 +0,0 @@
-import { useEffect } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-
-import { createNotification } from "@app/components/notifications";
-import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
- FormControl,
- Input,
- Modal,
- ModalContent,
- Select,
- SelectItem
-} from "@app/components/v2";
-import { useWorkspace } from "@app/context";
-import {
- useCreateAccessApprovalPolicy,
- useUpdateAccessApprovalPolicy
-} from "@app/hooks/api/accessApproval";
-import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
-import { TWorkspaceUser } from "@app/hooks/api/users/types";
-
-type Props = {
- isOpen?: boolean;
- onToggle: (isOpen: boolean) => void;
- members?: TWorkspaceUser[];
- projectSlug: string;
- editValues?: TAccessApprovalPolicy;
-};
-
-const formSchema = z
- .object({
- environment: z.string(),
- name: z.string().optional(),
- secretPath: z.string().optional(),
- approvals: z.number().min(1),
- approvers: z.string().array().min(1)
- })
- .refine((data) => data.approvals <= data.approvers.length, {
- path: ["approvals"],
- message: "The number of approvals should be lower than the number of approvers."
- });
-
-type TFormSchema = z.infer;
-
-export const AccessPolicyForm = ({
- isOpen,
- onToggle,
- members = [],
- projectSlug,
- editValues
-}: Props) => {
- const {
- control,
- handleSubmit,
- reset,
- formState: { isSubmitting }
- } = useForm({
- resolver: zodResolver(formSchema),
- values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
- });
- const { currentWorkspace } = useWorkspace();
-
- const environments = currentWorkspace?.environments || [];
- useEffect(() => {
- if (!isOpen) reset({});
- }, [isOpen]);
-
- const isEditMode = Boolean(editValues);
-
- const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
- const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
-
- const handleCreatePolicy = async (data: TFormSchema) => {
- if (!projectSlug) return;
-
- try {
- await createAccessApprovalPolicy({
- ...data,
- projectSlug
- });
- createNotification({
- type: "success",
- text: "Successfully created policy"
- });
- onToggle(false);
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "Failed to create policy"
- });
- }
- };
-
- const handleUpdatePolicy = async (data: TFormSchema) => {
- if (!projectSlug) return;
- if (!editValues?.id) return;
-
- try {
- await updateAccessApprovalPolicy({
- id: editValues?.id,
- ...data,
- projectSlug
- });
- createNotification({
- type: "success",
- text: "Successfully updated policy"
- });
- onToggle(false);
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "failed to update policy"
- });
- }
- };
-
- const handleFormSubmit = async (data: TFormSchema) => {
- if (isEditMode) {
- await handleUpdatePolicy(data);
- } else {
- await handleCreatePolicy(data);
- }
- };
-
- return (
-
-
-
-
-
- );
-};
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx
deleted file mode 100644
index f6db07c94..000000000
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { AccessApprovalPolicyList } from "./AccessApprovalPolicyList";
From 869fcd6541698cd965a52858b691f8bc18830bc9 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 21:30:55 +0200
Subject: [PATCH 08/30] feat(frontend): remove SecretApprovalPolicyList
---
.../SecretApprovalPage/SecretApprovalPage.tsx | 7 -
.../SecretApprovalPolicyList.tsx | 178 ------------
.../components/SecretApprovalPolicyRow.tsx | 148 ----------
.../components/SecretPolicyForm.tsx | 262 ------------------
.../SecretApprovalPolicyList/index.tsx | 1 -
5 files changed, 596 deletions(-)
delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx
delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx
delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx
delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx
diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
index df0ba55f9..8d7282130 100644
--- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
+++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
@@ -4,13 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
-import { Divider } from "@app/components/v2/Divider";
import { useWorkspace } from "@app/context";
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
import { AccessApprovalRequest } from "./components/AccessApprovalRequest";
import { ApprovalPolicyList } from "./components/ApprovalPolicyList";
-import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList";
import { SecretApprovalRequest } from "./components/SecretApprovalRequest";
enum TabSection {
@@ -59,17 +57,12 @@ export const SecretApprovalPage = () => {
Secret Requests
{Boolean(secretApprovalReqCount?.open) && ({secretApprovalReqCount?.open})}
- Secret Policies
-
Access Requests
{Boolean(accessApprovalRequestCount?.pendingCount) && {accessApprovalRequestCount?.pendingCount}}
Policies
-
-
-
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx
deleted file mode 100644
index f377f44e9..000000000
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { createNotification } from "@app/components/notifications";
-import { ProjectPermissionCan } from "@app/components/permissions";
-import {
- Button,
- DeleteActionModal,
- EmptyState,
- Modal,
- ModalContent,
- Table,
- TableContainer,
- TableSkeleton,
- TBody,
- Td,
- Th,
- THead,
- Tr,
- UpgradePlanModal
-} from "@app/components/v2";
-import {
- ProjectPermissionActions,
- ProjectPermissionSub,
- useProjectPermission,
- useSubscription
-} from "@app/context";
-import { usePopUp } from "@app/hooks";
-import {
- useDeleteSecretApprovalPolicy,
- useGetSecretApprovalPolicies,
- useGetWorkspaceUsers
-} from "@app/hooks/api";
-import { TSecretApprovalPolicy } from "@app/hooks/api/types";
-
-import { SecretApprovalPolicyRow } from "./components/SecretApprovalPolicyRow";
-import { SecretPolicyForm } from "./components/SecretPolicyForm";
-
-type Props = {
- workspaceId: string;
-};
-
-export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
- const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
- "secretPolicyForm",
- "deletePolicy",
- "upgradePlan"
- ] as const);
- const { permission } = useProjectPermission();
- const { subscription } = useSubscription();
-
- const { data: members } = useGetWorkspaceUsers(workspaceId);
- const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({
- workspaceId,
- options: {
- enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval)
- }
- });
-
- const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
-
- const handleDeletePolicy = async () => {
- const { id } = popUp.deletePolicy.data as TSecretApprovalPolicy;
- try {
- await deleteSecretApprovalPolicy({
- workspaceId,
- id
- });
- createNotification({
- type: "success",
- text: "Successfully deleted policy"
- });
- handlePopUpClose("deletePolicy");
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "Failed to delete policy"
- });
- }
- };
-
- return (
-
-
-
-
Approval Policies
-
- Implement policies to prevent unauthorized secret changes.
-
-
-
-
- {(isAllowed) => (
-
- )}
-
-
-
-
-
-
-
- | Name |
- Environment |
- Secret Path |
- Eligible Approvers |
- Approval Required |
-
-
-
- {isPoliciesLoading && (
-
- )}
- {!isPoliciesLoading && !policies?.length && (
-
- |
-
- |
-
- )}
- {policies?.map((policy) => (
- handlePopUpOpen("secretPolicyForm", policy)}
- onDelete={() => handlePopUpOpen("deletePolicy", policy)}
- />
- ))}
-
-
-
-
handlePopUpToggle("secretPolicyForm", isOpen)}
- >
-
- handlePopUpToggle("secretPolicyForm", isOpen)}
- members={members}
- editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
- />
-
-
-
handlePopUpToggle("deletePolicy", isOpen)}
- onDeleteApproved={handleDeletePolicy}
- />
- handlePopUpToggle("upgradePlan", isOpen)}
- text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
- />
-
- );
-};
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx
deleted file mode 100644
index 9f8a08c44..000000000
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { useState } from "react";
-import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-import { ProjectPermissionCan } from "@app/components/permissions";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
- IconButton,
- Input,
- Td,
- Tr
-} from "@app/components/v2";
-import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
-import { useUpdateSecretApprovalPolicy } from "@app/hooks/api";
-import { TSecretApprovalPolicy } from "@app/hooks/api/types";
-import { TWorkspaceUser } from "@app/hooks/api/users/types";
-
-type Props = {
- policy: TSecretApprovalPolicy;
- members?: TWorkspaceUser[];
- workspaceId: string;
- onEdit: () => void;
- onDelete: () => void;
-};
-
-export const SecretApprovalPolicyRow = ({
- policy,
- members = [],
- workspaceId,
- onEdit,
- onDelete
-}: Props) => {
- const [selectedApprovers, setSelectedApprovers] = useState([]);
- const { mutate: updateSecretApprovalPolicy, isLoading } = useUpdateSecretApprovalPolicy();
- const { permission } = useProjectPermission();
-
- return (
-
- | {policy.name} |
- {policy.environment.slug} |
- {policy.secretPath || "*"} |
-
- {
- if (!isOpen) {
- updateSecretApprovalPolicy(
- {
- workspaceId,
- id: policy.id,
- approvers: selectedApprovers
- },
- {
- onSettled: () => {
- setSelectedApprovers([]);
- }
- }
- );
- } else {
- setSelectedApprovers(policy.userApprovers.map(({ userId }) => userId));
- }
- }}
- >
-
-
-
-
-
- Select members that are allowed to approve changes
-
- {members?.map(({ user }) => {
- const isChecked = selectedApprovers.includes(user.id);
- return (
- {
- evt.preventDefault();
- setSelectedApprovers((state) =>
- isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
- );
- }}
- key={`create-policy-members-${user.id}`}
- iconPos="right"
- icon={isChecked && }
- >
- {user.username}
-
- );
- })}
-
-
- |
- {policy.approvals} |
-
-
-
- {(isAllowed) => (
-
-
-
- )}
-
-
- {(isAllowed) => (
-
-
-
- )}
-
-
- |
-
- );
-};
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx
deleted file mode 100644
index 18b0b87c8..000000000
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import { Controller, useForm } from "react-hook-form";
-import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-
-import { createNotification } from "@app/components/notifications";
-import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
- FormControl,
- Input,
- Select,
- SelectItem
-} from "@app/components/v2";
-import { SecretPathInput } from "@app/components/v2/SecretPathInput";
-import { useWorkspace } from "@app/context";
-import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
-import { TSecretApprovalPolicy } from "@app/hooks/api/types";
-import { TWorkspaceUser } from "@app/hooks/api/users/types";
-
-type Props = {
- isOpen?: boolean;
- onToggle: (isOpen: boolean) => void;
- members?: TWorkspaceUser[];
- workspaceId: string;
- editValues?: TSecretApprovalPolicy;
-};
-
-const formSchema = z
- .object({
- environment: z.string(),
- name: z.string().optional(),
- secretPath: z.string().optional().nullable(),
- approvals: z.number().min(1),
- approvers: z.string().array().min(1)
- })
- .refine((data) => data.approvals <= data.approvers.length, {
- path: ["approvals"],
- message: "The number of approvals should be lower than the number of approvers."
- });
-
-type TFormSchema = z.infer;
-
-export const SecretPolicyForm = ({
- onToggle,
- members = [],
- workspaceId,
- editValues
-}: Props) => {
- const {
- control,
- handleSubmit,
- watch,
- formState: { isSubmitting }
- } = useForm({
- resolver: zodResolver(formSchema),
- values: editValues
- ? {
- ...editValues,
- approvers: editValues.userApprovers.map(({ userId }) => userId),
- environment: editValues.environment.slug
- }
- : undefined
- });
- const { currentWorkspace } = useWorkspace();
- const selectedEnvironment = watch("environment");
-
- const environments = currentWorkspace?.environments || [];
-
- const isEditMode = Boolean(editValues);
-
- const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
- const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
-
- const handleCreatePolicy = async (data: TFormSchema) => {
- try {
- await createSecretApprovalPolicy({
- ...data,
- workspaceId
- });
- createNotification({
- type: "success",
- text: "Successfully created policy"
- });
- onToggle(false);
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "Failed to create policy"
- });
- }
- };
-
- const handleUpdatePolicy = async (data: TFormSchema) => {
- if (!editValues?.id) return;
- try {
- await updateSecretApprovalPolicy({
- id: editValues?.id,
- ...data,
- secretPath: data.secretPath || null,
- workspaceId
- });
- createNotification({
- type: "success",
- text: "Successfully updated policy"
- });
- onToggle(false);
- } catch (err) {
- console.log(err);
- createNotification({
- type: "error",
- text: "failed to update policy"
- });
- }
- };
-
- const handleFormSubmit = async (data: TFormSchema) => {
- if (isEditMode) {
- await handleUpdatePolicy(data);
- } else {
- await handleCreatePolicy(data);
- }
- };
-
- return (
-
-
- );
-};
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx
deleted file mode 100644
index f204264b4..000000000
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { SecretApprovalPolicyList } from "./SecretApprovalPolicyList";
From 2c57bd94fb00f27a316626b292834b4030e00751 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 21:39:33 +0200
Subject: [PATCH 09/30] feat(backend): add enforcementLevel into
secret_approval_policies
---
...4929_add-enforcement-level-secrets-policies.ts | 15 +++++++++++++++
.../src/db/schemas/secret-approval-policies.ts | 3 +++
.../ee/routes/v1/secret-approval-policy-router.ts | 10 +++++++---
.../secret-approval-policy-service.ts | 12 ++++++++----
.../secret-approval-policy-types.ts | 4 +++-
backend/src/lib/types/index.ts | 5 +++++
6 files changed, 41 insertions(+), 8 deletions(-)
create mode 100644 backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts
diff --git a/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts
new file mode 100644
index 000000000..4a1df888a
--- /dev/null
+++ b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts
@@ -0,0 +1,15 @@
+import { Knex } from "knex";
+
+import { EnforcementLevel } from "@app/lib/types";
+
+export async function up(knex: Knex): Promise {
+ await knex.schema.table("secret_approval_policies", (table) => {
+ table.specificType("enforcementLevel", "VARCHAR(10)").notNullable().defaultTo(EnforcementLevel.Hard);
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.table("secret_approval_policies", (table) => {
+ table.dropColumn("enforcementLevel");
+ });
+}
diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts
index d907ef1e0..bac9c4b2e 100644
--- a/backend/src/db/schemas/secret-approval-policies.ts
+++ b/backend/src/db/schemas/secret-approval-policies.ts
@@ -5,6 +5,8 @@
import { z } from "zod";
+import { EnforcementLevel } from "@app/lib/types";
+
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesSchema = z.object({
@@ -12,6 +14,7 @@ export const SecretApprovalPoliciesSchema = z.object({
name: z.string(),
secretPath: z.string().nullable().optional(),
approvals: z.number().default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel),
envId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
index f25ae42f3..305c588a6 100644
--- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
@@ -2,6 +2,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { removeTrailingSlash } from "@app/lib/fn";
+import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
@@ -26,7 +27,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.nullable()
.transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z.string().array().min(1),
- approvals: z.number().min(1).default(1)
+ approvals: z.number().min(1).default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
@@ -47,7 +49,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body,
- name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
+ name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
+ enforcementLevel: req.body.enforcementLevel ?? EnforcementLevel.Hard
});
return { approval };
}
@@ -72,7 +75,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.string()
.optional()
.nullable()
- .transform((val) => (val ? removeTrailingSlash(val) : val))
+ .transform((val) => (val ? removeTrailingSlash(val) : val)),
+ enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
index be6a334e2..366ad69ea 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
@@ -48,7 +48,8 @@ export const secretApprovalPolicyServiceFactory = ({
approvers,
projectId,
secretPath,
- environment
+ environment,
+ enforcementLevel
}: TCreateSapDTO) => {
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
@@ -73,7 +74,8 @@ export const secretApprovalPolicyServiceFactory = ({
envId: env.id,
approvals,
secretPath,
- name
+ name,
+ enforcementLevel
},
tx
);
@@ -98,7 +100,8 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId,
actorAuthMethod,
approvals,
- secretPolicyId
+ secretPolicyId,
+ enforcementLevel
}: TUpdateSapDTO) => {
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
@@ -118,7 +121,8 @@ export const secretApprovalPolicyServiceFactory = ({
{
approvals,
secretPath,
- name
+ name,
+ enforcementLevel
},
tx
);
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
index 2ddd9b51b..8e7099c98 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
@@ -1,4 +1,4 @@
-import { TProjectPermission } from "@app/lib/types";
+import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
export type TCreateSapDTO = {
approvals: number;
@@ -7,6 +7,7 @@ export type TCreateSapDTO = {
approvers: string[];
projectId: string;
name: string;
+ enforcementLevel: EnforcementLevel;
} & Omit;
export type TUpdateSapDTO = {
@@ -15,6 +16,7 @@ export type TUpdateSapDTO = {
secretPath?: string | null;
approvers: string[];
name?: string;
+ enforcementLevel?: EnforcementLevel;
} & Omit;
export type TDeleteSapDTO = {
diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts
index 2c41f4d23..382762aaa 100644
--- a/backend/src/lib/types/index.ts
+++ b/backend/src/lib/types/index.ts
@@ -42,3 +42,8 @@ export type RequiredKeys = {
}[keyof T];
export type PickRequired = Pick>;
+
+export enum EnforcementLevel {
+ Hard = "hard",
+ Soft = "soft"
+}
From f3f87cfd84460e3aea5309c55fa35b32f681728f Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 21:43:53 +0200
Subject: [PATCH 10/30] feat(frontend): add enforcementLevel into SecretPolicy
---
.../src/hooks/api/secretApproval/mutation.tsx | 10 ++++++----
frontend/src/hooks/api/secretApproval/types.ts | 3 +++
.../components/AccessPolicyModal.tsx | 10 +++++-----
.../components/ApprovalPolicyRow.tsx | 15 ++++-----------
4 files changed, 18 insertions(+), 20 deletions(-)
diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx
index e0a8df95d..2ad79932b 100644
--- a/frontend/src/hooks/api/secretApproval/mutation.tsx
+++ b/frontend/src/hooks/api/secretApproval/mutation.tsx
@@ -9,14 +9,15 @@ export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
- mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
+ mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name, enforcementLevel }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
workspaceId,
approvals,
approvers,
secretPath,
- name
+ name,
+ enforcementLevel
});
return data;
},
@@ -30,12 +31,13 @@ export const useUpdateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
- mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
+ mutationFn: async ({ id, approvers, approvals, secretPath, name, enforcementLevel }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
approvers,
secretPath,
- name
+ name,
+ enforcementLevel
});
return data;
},
diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts
index bc0d57d40..2a64ccdc9 100644
--- a/frontend/src/hooks/api/secretApproval/types.ts
+++ b/frontend/src/hooks/api/secretApproval/types.ts
@@ -1,3 +1,4 @@
+import { EnforcementLevel } from "../policies/enums";
import { WorkspaceEnv } from "../workspace/types";
export type TSecretApprovalPolicy = {
@@ -29,6 +30,7 @@ export type TCreateSecretPolicyDTO = {
secretPath?: string | null;
approvers?: string[];
approvals?: number;
+ enforcementLevel: EnforcementLevel;
};
export type TUpdateSecretPolicyDTO = {
@@ -37,6 +39,7 @@ export type TUpdateSecretPolicyDTO = {
approvers?: string[];
secretPath?: string | null;
approvals?: number;
+ enforcementLevel?: EnforcementLevel;
// for invalidating list
workspaceId: string;
};
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
index 9ebd36661..54d88ed08 100644
--- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
@@ -143,12 +143,12 @@ export const AccessPolicyForm = ({
...data,
projectSlug
});
- createNotification({
- type: "success",
- text: "Successfully updated policy"
- });
- onToggle(false);
}
+ createNotification({
+ type: "success",
+ text: "Successfully updated policy"
+ });
+ onToggle(false);
} catch (err) {
console.log(err);
createNotification({
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
index e251f88ca..3aa5ce486 100644
--- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
@@ -18,7 +18,7 @@ import { Badge } from "@app/components/v2/Badge";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { policyDetails } from "@app/helpers/policies";
import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
-import { PolicyType } from "@app/hooks/api/policies/enums";
+import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { TWorkspaceUser } from "@app/hooks/api/users/types";
@@ -33,6 +33,7 @@ interface IPolicy {
userApprovers?: { userId: string }[];
updatedAt: Date;
policyType: PolicyType;
+ enforcementLevel: EnforcementLevel;
};
type Props = {
@@ -75,11 +76,7 @@ export const ApprovalPolicyRow = ({
id: policy.id,
approvers: selectedApprovers
},
- {
- onSettled: () => {
- // No changes needed here
- }
- }
+ { onSettled: () => {} }
);
} else {
updateSecretApprovalPolicy(
@@ -88,11 +85,7 @@ export const ApprovalPolicyRow = ({
id: policy.id,
approvers: selectedApprovers
},
- {
- onSettled: () => {
- // No changes needed here
- }
- }
+ { onSettled: () => {} }
);
}
} else {
From a538e37a62174cee0d22f69038ce0aad4e1d9716 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 21:48:57 +0200
Subject: [PATCH 11/30] chore(backend): schema secret_approval_policies
---
backend/src/db/schemas/secret-approval-policies.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts
index bac9c4b2e..94aeba050 100644
--- a/backend/src/db/schemas/secret-approval-policies.ts
+++ b/backend/src/db/schemas/secret-approval-policies.ts
@@ -5,8 +5,6 @@
import { z } from "zod";
-import { EnforcementLevel } from "@app/lib/types";
-
import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesSchema = z.object({
@@ -14,10 +12,10 @@ export const SecretApprovalPoliciesSchema = z.object({
name: z.string(),
secretPath: z.string().nullable().optional(),
approvals: z.number().default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel),
envId: z.string().uuid(),
createdAt: z.date(),
- updatedAt: z.date()
+ updatedAt: z.date(),
+ enforcementLevel: z.string().default("hard")
});
export type TSecretApprovalPolicies = z.infer;
From d4dd684f320e4c3b2bd287ddd231244be461bf9f Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 22:30:55 +0200
Subject: [PATCH 12/30] feat(backend): add enforcementLevel into
access_approval_policies
---
...94958_add-enforcement-level-access-policies.ts | 15 +++++++++++++++
.../src/db/schemas/access-approval-policies.ts | 5 ++++-
.../ee/routes/v1/access-approval-policy-router.ts | 10 +++++++---
.../access-approval-policy-service.ts | 12 ++++++++----
.../access-approval-policy-types.ts | 4 +++-
5 files changed, 37 insertions(+), 9 deletions(-)
create mode 100644 backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts
diff --git a/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts
new file mode 100644
index 000000000..9aeb8d51e
--- /dev/null
+++ b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts
@@ -0,0 +1,15 @@
+import { Knex } from "knex";
+
+import { EnforcementLevel } from "@app/lib/types";
+
+export async function up(knex: Knex): Promise {
+ await knex.schema.table("access_approval_policies", (table) => {
+ table.specificType("enforcementLevel", "VARCHAR(10)").notNullable().defaultTo(EnforcementLevel.Hard);
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ await knex.schema.table("access_approval_policies", (table) => {
+ table.dropColumn("enforcementLevel");
+ });
+}
diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts
index 69068d23b..c05f22b31 100644
--- a/backend/src/db/schemas/access-approval-policies.ts
+++ b/backend/src/db/schemas/access-approval-policies.ts
@@ -5,6 +5,8 @@
import { z } from "zod";
+import { EnforcementLevel } from "@app/lib/types";
+
import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesSchema = z.object({
@@ -14,7 +16,8 @@ export const AccessApprovalPoliciesSchema = z.object({
secretPath: z.string().nullable().optional(),
envId: z.string().uuid(),
createdAt: z.date(),
- updatedAt: z.date()
+ updatedAt: z.date(),
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
});
export type TAccessApprovalPolicies = z.infer;
diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts
index 3b8949d3b..fe3a16f2b 100644
--- a/backend/src/ee/routes/v1/access-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts
@@ -1,6 +1,7 @@
import { nanoid } from "nanoid";
import { z } from "zod";
+import { EnforcementLevel } from "@app/lib/types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -17,7 +18,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
secretPath: z.string().trim().default("/"),
environment: z.string(),
approvers: z.string().array().min(1),
- approvals: z.number().min(1).default(1)
+ approvals: z.number().min(1).default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
@@ -38,7 +40,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
...req.body,
projectSlug: req.body.projectSlug,
- name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
+ name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
+ enforcementLevel: req.body.enforcementLevel
});
return { approval };
}
@@ -115,7 +118,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.optional()
.transform((val) => (val === "" ? "/" : val)),
approvers: z.string().array().min(1),
- approvals: z.number().min(1).default(1)
+ approvals: z.number().min(1).default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
index 51a51abb5..c91fbf970 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
@@ -47,7 +47,8 @@ export const accessApprovalPolicyServiceFactory = ({
approvals,
approvers,
projectSlug,
- environment
+ environment,
+ enforcementLevel
}: TCreateAccessApprovalPolicy) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
@@ -94,7 +95,8 @@ export const accessApprovalPolicyServiceFactory = ({
envId: env.id,
approvals,
secretPath,
- name
+ name,
+ enforcementLevel
},
tx
);
@@ -143,7 +145,8 @@ export const accessApprovalPolicyServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
- approvals
+ approvals,
+ enforcementLevel
}: TUpdateAccessApprovalPolicy) => {
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
@@ -163,7 +166,8 @@ export const accessApprovalPolicyServiceFactory = ({
{
approvals,
secretPath,
- name
+ name,
+ enforcementLevel
},
tx
);
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
index 601561b68..fdb6fc8bb 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
@@ -1,4 +1,4 @@
-import { TProjectPermission } from "@app/lib/types";
+import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type";
import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -20,6 +20,7 @@ export type TCreateAccessApprovalPolicy = {
approvers: string[];
projectSlug: string;
name: string;
+ enforcementLevel: EnforcementLevel;
} & Omit;
export type TUpdateAccessApprovalPolicy = {
@@ -28,6 +29,7 @@ export type TUpdateAccessApprovalPolicy = {
approvers?: string[];
secretPath?: string;
name?: string;
+ enforcementLevel?: EnforcementLevel;
} & Omit;
export type TDeleteAccessApprovalPolicy = {
From c331af5345ea8871551f3634780ff16daee42258 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Wed, 17 Jul 2024 22:32:30 +0200
Subject: [PATCH 13/30] feat(frontend): add enforcementLevel into AccessPolicy
---
frontend/src/hooks/api/accessApproval/mutation.tsx | 10 ++++++----
frontend/src/hooks/api/accessApproval/types.ts | 2 ++
frontend/src/hooks/api/secretApproval/types.ts | 1 +
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx
index 5f595c8a2..251e1c626 100644
--- a/frontend/src/hooks/api/accessApproval/mutation.tsx
+++ b/frontend/src/hooks/api/accessApproval/mutation.tsx
@@ -16,14 +16,15 @@ export const useCreateAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateAccessPolicyDTO>({
- mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => {
+ mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath, enforcementLevel }) => {
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
environment,
projectSlug,
approvals,
approvers,
secretPath,
- name
+ name,
+ enforcementLevel
});
return data;
},
@@ -37,12 +38,13 @@ export const useUpdateAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
- mutationFn: async ({ id, approvers, approvals, name, secretPath }) => {
+ mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => {
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
approvals,
approvers,
secretPath,
- name
+ name,
+ enforcementLevel
});
return data;
},
diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts
index a6d09a227..f023b3555 100644
--- a/frontend/src/hooks/api/accessApproval/types.ts
+++ b/frontend/src/hooks/api/accessApproval/types.ts
@@ -125,6 +125,7 @@ export type TCreateAccessPolicyDTO = {
approvers?: string[];
approvals?: number;
secretPath?: string;
+ enforcementLevel?: EnforcementLevel;
};
export type TUpdateAccessPolicyDTO = {
@@ -134,6 +135,7 @@ export type TUpdateAccessPolicyDTO = {
secretPath?: string;
environment?: string;
approvals?: number;
+ enforcementLevel?: EnforcementLevel;
// for invalidating list
projectSlug: string;
};
diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts
index 2a64ccdc9..06ffad432 100644
--- a/frontend/src/hooks/api/secretApproval/types.ts
+++ b/frontend/src/hooks/api/secretApproval/types.ts
@@ -11,6 +11,7 @@ export type TSecretApprovalPolicy = {
approvals: number;
userApprovers: { userId: string }[];
updatedAt: Date;
+ enforcementLevel: EnforcementLevel;
};
export type TGetSecretApprovalPoliciesDTO = {
From aed310b9ee1efadb144efaf579c6659300136bc6 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 01:02:57 +0200
Subject: [PATCH 14/30] feat(backemd): accept soft approvals on secret requests
---
.../src/ee/routes/v1/secret-approval-request-router.ts | 6 ++++--
.../secret-approval-request-dal.ts | 8 ++++++--
.../secret-approval-request-service.ts | 6 +++++-
3 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts
index 8e72597bd..8e0b8a682 100644
--- a/backend/src/ee/routes/v1/secret-approval-request-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts
@@ -49,7 +49,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
name: z.string(),
approvals: z.number(),
approvers: z.string().array(),
- secretPath: z.string().optional().nullable()
+ secretPath: z.string().optional().nullable(),
+ enforcementLevel: z.string()
}),
committerUser: approvalRequestUser,
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
@@ -248,7 +249,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
name: z.string(),
approvals: z.number(),
approvers: approvalRequestUser.array(),
- secretPath: z.string().optional().nullable()
+ secretPath: z.string().optional().nullable(),
+ enforcementLevel: z.string()
}),
environment: z.string(),
statusChangedByUser: approvalRequestUser.optional(),
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
index 06c48ac8b..4ce174d26 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
@@ -94,6 +94,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
+ tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
);
@@ -128,7 +129,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id: el.policyId,
name: el.policyName,
approvals: el.policyApprovals,
- secretPath: el.policySecretPath
+ secretPath: el.policySecretPath,
+ enforcementLevel: el.policyEnforcementLevel
}
}),
childrenMapper: [
@@ -282,6 +284,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
),
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
+ db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
@@ -308,7 +311,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
id: el.policyId,
name: el.policyName,
approvals: el.policyApprovals,
- secretPath: el.policySecretPath
+ secretPath: el.policySecretPath,
+ enforcementLevel: el.policyEnforcementLevel
},
committerUser: {
userId: el.committerUserId,
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index a519af4fd..ae9e54b46 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -11,6 +11,7 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { groupBy, pick, unique } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
+import { EnforcementLevel } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -289,7 +290,10 @@ export const secretApprovalRequestServiceFactory = ({
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
- if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
+ const isSoftPolicy = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
+
+ if (!hasMinApproval && !isSoftPolicy)
+ throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
From 8a5a295a0100faf37b66af3d16d39f1093287a03 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 01:30:40 +0200
Subject: [PATCH 15/30] feat(frontend): accept soft approvals on secret
requests
---
.../ApprovalPolicyList/ApprovalPolicyList.tsx | 2 +-
.../SecretApprovalRequestAction.tsx | 39 +++++++++++++++----
.../SecretApprovalRequestChanges.tsx | 1 +
3 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx
index fb21521c9..2639f2351 100644
--- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx
@@ -177,7 +177,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
}
>
Type
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
index 8e9133e5b..05db75a9d 100644
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
@@ -1,20 +1,22 @@
+import React, { useState } from "react";
import {
faCheck,
faClose,
+ faLandMineOn,
faLockOpen,
faSquareCheck,
faSquareXmark,
- faUserLock
-} from "@fortawesome/free-solid-svg-icons";
+ faUserLock} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
-import { Button } from "@app/components/v2";
+import { Button, Checkbox } from "@app/components/v2";
import {
usePerformSecretApprovalRequestMerge,
useUpdateSecretApprovalRequestStatus
} from "@app/hooks/api";
+import { EnforcementLevel } from "@app/hooks/api/policies/enums";
type Props = {
approvalRequestId: string;
@@ -25,6 +27,7 @@ type Props = {
canApprove?: boolean;
statusChangeByEmail?: string;
workspaceId: string;
+ enforcementLevel: EnforcementLevel;
};
export const SecretApprovalRequestAction = ({
@@ -34,7 +37,8 @@ export const SecretApprovalRequestAction = ({
isMergable,
approvals,
statusChangeByEmail,
- workspaceId,
+ workspaceId,
+ enforcementLevel,
canApprove
}: Props) => {
const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } =
@@ -43,6 +47,8 @@ export const SecretApprovalRequestAction = ({
const { mutateAsync: updateSecretStatusChange, isLoading: isStatusChanging } =
useUpdateSecretApprovalRequestStatus();
+ const [byPassApproval, setByPassApproval] = useState(false);
+
const handleSecretApprovalRequestMerge = async () => {
try {
await performSecretApprovalMerge({
@@ -82,6 +88,8 @@ export const SecretApprovalRequestAction = ({
}
};
+ const isSoftEnforcement = enforcementLevel === EnforcementLevel.Soft;
+
if (!hasMerged && status === "open") {
return (
@@ -96,10 +104,25 @@ export const SecretApprovalRequestAction = ({
At least {approvals} approving review required
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
+ {!canApprove && isSoftEnforcement && (
+
+ setByPassApproval(checked === true)}
+ isChecked={byPassApproval}
+ id="byPassApproval"
+ checkIndicatorBg="text-white"
+ className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
+ >
+
+ Merge without waiting for approval (by pass secrets protection)
+
+
+
+ )}
- {canApprove ? (
+ {canApprove || isSoftEnforcement ? (
<>
}
- isDisabled={!isMergable}
+ leftIcon={}
+ isDisabled={!isMergable || (!byPassApproval && !canApprove)}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
- colorSchema="primary"
+ colorSchema={isSoftEnforcement && !canApprove ? "danger" : "primary"}
variant="solid"
>
Merge
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx
index 4d3ddef9c..7e8e30483 100644
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx
@@ -252,6 +252,7 @@ export const SecretApprovalRequestChanges = ({
status={secretApprovalRequestDetails.status}
isMergable={isMergable}
statusChangeByEmail={secretApprovalRequestDetails.statusChangedByUser?.email}
+ enforcementLevel={secretApprovalRequestDetails.policy.enforcementLevel}
workspaceId={workspaceId}
/>
From f464d7a09606da05f3410f480d5b04484d449844 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 02:25:12 +0200
Subject: [PATCH 16/30] feat(backend): accept soft approvals on access requests
---
backend/src/ee/routes/v1/access-approval-request-router.ts | 3 ++-
.../access-approval-request/access-approval-request-dal.ts | 6 +++++-
.../secret-approval-request-service.ts | 4 ++--
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts
index 4b173cfa7..972289b04 100644
--- a/backend/src/ee/routes/v1/access-approval-request-router.ts
+++ b/backend/src/ee/routes/v1/access-approval-request-router.ts
@@ -99,7 +99,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
approvals: z.number(),
approvers: z.string().array(),
secretPath: z.string().nullish(),
- envId: z.string()
+ envId: z.string(),
+ enforcementLevel: z.string()
}),
reviewers: z
.object({
diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
index c3c0d24d0..b293a38d6 100644
--- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
+++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
@@ -48,6 +48,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
+ db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
)
@@ -98,6 +99,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
name: doc.policyName,
approvals: doc.policyApprovals,
secretPath: doc.policySecretPath,
+ enforcementLevel: doc.policyEnforcementLevel,
envId: doc.policyEnvId
},
privilege: doc.privilegeId
@@ -165,6 +167,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
+ tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
);
@@ -184,7 +187,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
id: el.policyId,
name: el.policyName,
approvals: el.policyApprovals,
- secretPath: el.policySecretPath
+ secretPath: el.policySecretPath,
+ enforcementLevel: el.policyEnforcementLevel
}
}),
childrenMapper: [
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index ae9e54b46..9f824b2b1 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -290,9 +290,9 @@ export const secretApprovalRequestServiceFactory = ({
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
- const isSoftPolicy = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
+ const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
- if (!hasMinApproval && !isSoftPolicy)
+ if (!hasMinApproval && !isSoftEnforcement)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
From 454826fbb64b2616e3cb2cfe20b1fba8fbe71d8a Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 02:25:53 +0200
Subject: [PATCH 17/30] feat(frontend): accept soft approvals on access
requests
---
.../src/hooks/api/accessApproval/types.ts | 1 +
.../AccessApprovalRequest.tsx | 32 +++++++++++++++----
.../components/ReviewAccessModal.tsx | 32 +++++++++++++++++--
3 files changed, 55 insertions(+), 10 deletions(-)
diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts
index f023b3555..e17fe41d0 100644
--- a/frontend/src/hooks/api/accessApproval/types.ts
+++ b/frontend/src/hooks/api/accessApproval/types.ts
@@ -53,6 +53,7 @@ export type TAccessApprovalRequest = {
approvers: string[];
secretPath?: string | null;
envId: string;
+ enforcementLevel: EnforcementLevel;
};
reviewers: {
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
index 235737d4a..6851fdb6c 100644
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
@@ -40,6 +40,7 @@ import {
useGetAccessRequestsCount
} from "@app/hooks/api/accessApproval/queries";
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
+import { EnforcementLevel } from "@app/hooks/api/policies/enums";
import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
import { queryClient } from "@app/reactQuery";
@@ -80,7 +81,12 @@ export const AccessApprovalRequest = ({
projectId: string;
}) => {
const [selectedRequest, setSelectedRequest] = useState<
- (TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }) | null
+ (TAccessApprovalRequest & {
+ user: TWorkspaceUser["user"] | null;
+ isRequestedByCurrentUser: boolean;
+ isApprover: boolean;
+ })
+ | null
>(null);
const { handlePopUpOpen, popUp, handlePopUpClose } = usePopUp([
@@ -141,6 +147,8 @@ export const AccessApprovalRequest = ({
);
const isApprover = request.policy.approvers.indexOf(membership.id || "") !== -1;
const isAccepted = request.isApproved;
+ const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
+ const isRequestedByCurrentUser = request.requestedBy === membership.id;
const userReviewStatus = request.reviewers.find(
({ member }) => member === membership.id
@@ -178,7 +186,9 @@ export const AccessApprovalRequest = ({
isRejectedByAnyone,
isApprover,
userReviewStatus,
- isAccepted
+ isAccepted,
+ isSoftEnforcement,
+ isRequestedByCurrentUser
};
};
@@ -331,16 +341,24 @@ export const AccessApprovalRequest = ({
tabIndex={0}
onClick={() => {
if (
- !details.isApprover ||
- details.isReviewedByUser ||
- details.isRejectedByAnyone ||
- details.isAccepted
+ (
+ !details.isApprover
+ || details.isReviewedByUser
+ || details.isRejectedByAnyone
+ || details.isAccepted
+ ) && !(
+ details.isSoftEnforcement
+ && details.isRequestedByCurrentUser
+ && !details.isAccepted
+ )
)
return;
setSelectedRequest({
...request,
- user: membersGroupById?.[request.requestedBy].user!
+ user: membersGroupById?.[request.requestedBy].user!,
+ isRequestedByCurrentUser: details.isRequestedByCurrentUser,
+ isApprover: details.isApprover
});
handlePopUpOpen("reviewRequest");
}}
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
index 481cee9c6..10d0f2a8d 100644
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
@@ -2,11 +2,12 @@ import { useCallback, useMemo, useState } from "react";
import ms from "ms";
import { createNotification } from "@app/components/notifications";
-import { Button, Modal, ModalContent } from "@app/components/v2";
+import { Button, Checkbox, Modal, ModalContent } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
import { ProjectPermissionActions } from "@app/context";
import { useReviewAccessRequest } from "@app/hooks/api";
import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types";
+import { EnforcementLevel } from "@app/hooks/api/policies/enums";
import { TWorkspaceUser } from "@app/hooks/api/types";
export const ReviewAccessRequestModal = ({
@@ -19,12 +20,18 @@ export const ReviewAccessRequestModal = ({
}: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
- request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null };
+ request: TAccessApprovalRequest & {
+ user: TWorkspaceUser["user"] | null;
+ isRequestedByCurrentUser: boolean;
+ isApprover: boolean;
+ };
projectSlug: string;
selectedRequester: string | undefined;
selectedEnvSlug: string | undefined;
}) => {
const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null);
+ const [byPassApproval, setByPassApproval] = useState(false);
+ const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
const accessDetails = {
env: request.environmentName,
@@ -134,10 +141,14 @@ export const ReviewAccessRequestModal = ({
@@ -151,6 +162,21 @@ export const ReviewAccessRequestModal = ({
Reject Request
+ {isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && (
+
+ setByPassApproval(checked === true)}
+ isChecked={byPassApproval}
+ id="byPassApproval"
+ checkIndicatorBg="text-white"
+ className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
+ >
+
+ Approve without waiting for requirements to be met (by pass secrets protection)
+
+
+
+ )}
From d301f74feba71e4919664e0712cc7ea226e75978 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 02:46:50 +0200
Subject: [PATCH 18/30] fix(frontend): interactions SecretApprovalRequestAction
---
.../components/SecretApprovalRequestAction.tsx | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
index 05db75a9d..f56695764 100644
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
@@ -134,8 +134,11 @@ export const SecretApprovalRequestAction = ({
Close request
}
- isDisabled={!isMergable || (!byPassApproval && !canApprove)}
+ leftIcon={}
+ isDisabled={
+ (!isMergable && canApprove)
+ || (!canApprove && isSoftEnforcement && !byPassApproval)
+ }
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
colorSchema={isSoftEnforcement && !canApprove ? "danger" : "primary"}
From b70bf4cadb1b4ca9b90447b8f4b9d94b0f0e36bc Mon Sep 17 00:00:00 2001
From: kasyap dharanikota <76957823+kasyap1234@users.noreply.github.com>
Date: Thu, 18 Jul 2024 06:23:24 +0530
Subject: [PATCH 19/30] fix SMTP2GO configuration
---
docs/self-hosting/configuration/envars.mdx | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx
index 8719fa0e9..24ec3a372 100644
--- a/docs/self-hosting/configuration/envars.mdx
+++ b/docs/self-hosting/configuration/envars.mdx
@@ -332,10 +332,12 @@ SMTP_FROM_NAME=Infisical
1. Create an account and configure [SMTP2Go](https://www.smtp2go.com/) to send emails.
2. Turn on SMTP authentication
```
-SMTP server: mail.smtp2go.com
-SMTP port: You can use one of the following ports: 2525, 80, 25, 8025, or 587
-Username: Your SMTP2GO account's SMTP username
-Password: Your SMTP2GO account's SMTP password
+SMTP_HOST=mail.smtp2go.com
+SMTP_PORT=You can use one of the following ports: 2525, 80, 25, 8025, or 587
+SMTP_USERNAME=username #Your SMTP2GO account's SMTP username
+SMTP_PASSWORD=password #Your SMTP2GO account's SMTP password
+SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
+SMTP_FROM_NAME=Infisical
```
{" "}
From 498b1109c9d14f1e8275d9b49e8f66895b341d67 Mon Sep 17 00:00:00 2001
From: Tuan Dang
Date: Thu, 18 Jul 2024 10:32:39 +0700
Subject: [PATCH 20/30] resolve pr review issues
---
backend/src/server/routes/v2/organization-router.ts | 4 ++--
backend/src/services/org/org-service.ts | 9 +++++----
.../v2/DeleteActionModal/DeleteActionModal.tsx | 6 +++---
.../components/OrgMembersSection/OrgMembersTable.tsx | 2 +-
frontend/src/views/Org/UserPage/UserPage.tsx | 10 +++++-----
5 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts
index f87e7a6f0..49488776b 100644
--- a/backend/src/server/routes/v2/organization-router.ts
+++ b/backend/src/server/routes/v2/organization-router.ts
@@ -186,7 +186,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
@@ -289,7 +289,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
})
}
},
- onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const memberships = await server.services.org.listProjectMembershipsByOrgMembershipId({
actor: req.permission.type,
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index 3e30d8c52..289491e8d 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -379,6 +379,11 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
+ const [foundMembership] = await orgDAL.findMembership({ id: membershipId, orgId });
+ if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
+ if (foundMembership.userId === userId)
+ throw new BadRequestError({ message: "Cannot update own organization membership" });
+
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
if (role && isCustomRole) {
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
@@ -400,10 +405,6 @@ export const orgServiceFactory = ({
return membership;
}
- if (isActive !== undefined) {
- ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
- }
-
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive });
return membership;
};
diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
index 2657686de..76804c137 100644
--- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
+++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
@@ -25,7 +25,7 @@ export const DeleteActionModal = ({
deleteKey,
onDeleteApproved,
title,
- subTitle = "This action is irreversible!",
+ subTitle = "This action is irreversible.",
buttonText = "Delete"
}: Props): JSX.Element => {
const [inputData, setInputData] = useState("");
@@ -86,7 +86,7 @@ export const DeleteActionModal = ({
- Type {deleteKey} to delete the resource
+ Type {deleteKey} to perform this action
}
className="mb-0"
@@ -94,7 +94,7 @@ export const DeleteActionModal = ({
setInputData(e.target.value)}
- placeholder="Type to delete..."
+ placeholder="Type confirm..."
/>
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx
index ae75c5186..9212a5135 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from "react";
import { useRouter } from "next/router";
-import { faEllipsis,faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
+import { faEllipsis, faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
diff --git a/frontend/src/views/Org/UserPage/UserPage.tsx b/frontend/src/views/Org/UserPage/UserPage.tsx
index 7253d1c77..d27ba2365 100644
--- a/frontend/src/views/Org/UserPage/UserPage.tsx
+++ b/frontend/src/views/Org/UserPage/UserPage.tsx
@@ -25,7 +25,7 @@ import {
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
-import { UserDetailsSection, UserOrgMembershipModal,UserProjectsSection } from "./components";
+import { UserDetailsSection, UserOrgMembershipModal, UserProjectsSection } from "./components";
export const UserPage = withPermission(
() => {
@@ -161,9 +161,9 @@ export const UserPage = withPermission(
: ""
}
onClick={async () => {
- if (currentOrg?.authEnforced) {
+ if (currentOrg?.scimEnabled) {
createNotification({
- text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
+ text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
@@ -204,9 +204,9 @@ export const UserPage = withPermission(
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
- if (currentOrg?.authEnforced) {
+ if (currentOrg?.scimEnabled) {
createNotification({
- text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
+ text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
type: "error"
});
return;
From 4b718b679a09aee74b697c3c14f4a24463e01463 Mon Sep 17 00:00:00 2001
From: Tuan Dang
Date: Thu, 18 Jul 2024 10:44:54 +0700
Subject: [PATCH 21/30] Change deactivate button messaging, add scim check
---
.../components/OrgMembersSection/OrgMembersSection.tsx | 1 +
.../components/OrgMembersSection/OrgMembersTable.tsx | 4 ++--
frontend/src/views/Org/UserPage/UserPage.tsx | 1 +
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
index e4a54f1b5..c272145d5 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx
@@ -165,6 +165,7 @@ export const OrgMembersSection = () => {
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
+ buttonText="Deactivate"
/>
{
e.stopPropagation();
- if (currentOrg?.authEnforced) {
+ if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
@@ -338,7 +338,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
onClick={(e) => {
e.stopPropagation();
- if (currentOrg?.authEnforced) {
+ if (currentOrg?.scimEnabled) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"
diff --git a/frontend/src/views/Org/UserPage/UserPage.tsx b/frontend/src/views/Org/UserPage/UserPage.tsx
index d27ba2365..eb8b8e8e1 100644
--- a/frontend/src/views/Org/UserPage/UserPage.tsx
+++ b/frontend/src/views/Org/UserPage/UserPage.tsx
@@ -259,6 +259,7 @@ export const UserPage = withPermission(
(popUp?.deactivateMember?.data as { orgMembershipId: string })?.orgMembershipId
)
}
+ buttonText="Deactivate"
/>
Date: Thu, 18 Jul 2024 12:59:10 +0200
Subject: [PATCH 22/30] fix(frontend): types
---
.../AccessApprovalRequest/AccessApprovalRequest.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
index 6851fdb6c..b57c0c3ab 100644
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx
@@ -373,7 +373,9 @@ export const AccessApprovalRequest = ({
if (evt.key === "Enter") {
setSelectedRequest({
...request,
- user: membersGroupById?.[request.requestedBy].user!
+ user: membersGroupById?.[request.requestedBy].user!,
+ isRequestedByCurrentUser: details.isRequestedByCurrentUser,
+ isApprover: details.isApprover
});
handlePopUpOpen("reviewRequest");
}
From 997d7f22fcf5452b27b474afe60d1f8ae8dff15f Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 13:13:39 +0200
Subject: [PATCH 23/30] fix(backend): secretPath default / + default
enforcementLevel
---
.../src/ee/routes/v1/access-approval-policy-router.ts | 4 ++--
.../src/ee/routes/v1/secret-approval-policy-router.ts | 10 ++++++----
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts
index fe3a16f2b..8a3f93109 100644
--- a/backend/src/ee/routes/v1/access-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts
@@ -19,7 +19,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
environment: z.string(),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel)
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
@@ -119,7 +119,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.transform((val) => (val === "" ? "/" : val)),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel)
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
index 305c588a6..25c1bb0b5 100644
--- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
@@ -25,10 +25,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.string()
.optional()
.nullable()
+ .default("/")
.transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel)
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
@@ -50,7 +51,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
projectId: req.body.workspaceId,
...req.body,
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
- enforcementLevel: req.body.enforcementLevel ?? EnforcementLevel.Hard
+ enforcementLevel: req.body.enforcementLevel
});
return { approval };
}
@@ -75,8 +76,9 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.string()
.optional()
.nullable()
- .transform((val) => (val ? removeTrailingSlash(val) : val)),
- enforcementLevel: z.nativeEnum(EnforcementLevel)
+ .transform((val) => (val ? removeTrailingSlash(val) : val))
+ .transform((val) => (val === "" ? "/" : val)),
+ enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
From 43b14d0091d7016d726c09bae2bf6620da5147df Mon Sep 17 00:00:00 2001
From: Tuan Dang
Date: Thu, 18 Jul 2024 20:40:12 +0700
Subject: [PATCH 24/30] Patch for update org membership call, hide edit user on
self user page
---
backend/src/services/org/org-service.ts | 7 +-
frontend/src/views/Org/UserPage/UserPage.tsx | 222 +++++++++---------
.../UserProjectsSection.tsx | 4 +-
3 files changed, 123 insertions(+), 110 deletions(-)
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index 289491e8d..3ffc2e203 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -57,7 +57,7 @@ type TOrgServiceFactoryDep = {
projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick;
projectKeyDAL: Pick;
- orgMembershipDAL: Pick;
+ orgMembershipDAL: Pick;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick;
smtpService: TSmtpService;
@@ -379,7 +379,10 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
- const [foundMembership] = await orgDAL.findMembership({ id: membershipId, orgId });
+ const foundMembership = await orgMembershipDAL.findOne({
+ id: membershipId,
+ orgId
+ });
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
if (foundMembership.userId === userId)
throw new BadRequestError({ message: "Cannot update own organization membership" });
diff --git a/frontend/src/views/Org/UserPage/UserPage.tsx b/frontend/src/views/Org/UserPage/UserPage.tsx
index eb8b8e8e1..2858d97df 100644
--- a/frontend/src/views/Org/UserPage/UserPage.tsx
+++ b/frontend/src/views/Org/UserPage/UserPage.tsx
@@ -16,7 +16,12 @@ import {
Tooltip,
UpgradePlanModal
} from "@app/components/v2";
-import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
+import {
+ OrgPermissionActions,
+ OrgPermissionSubjects,
+ useOrganization,
+ useUser
+} from "@app/context";
import { withPermission } from "@app/hoc";
import {
useDeleteOrgMembership,
@@ -31,7 +36,10 @@ export const UserPage = withPermission(
() => {
const router = useRouter();
const membershipId = router.query.membershipId as string;
+ const { user } = useUser();
const { currentOrg } = useOrganization();
+
+ const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const { data: membership } = useGetOrgMembership(orgId, membershipId);
@@ -115,116 +123,118 @@ export const UserPage = withPermission(
? `${membership.user.firstName} ${membership.user.lastName}`
: "-"}
-
-
-
-
-
-
-
-
-
-
- {(isAllowed) => (
-
- handlePopUpOpen("orgMembership", {
- membershipId: membership.id,
- role: membership.role
- })
- }
- disabled={!isAllowed}
- >
- Edit User
-
- )}
-
-
- {(isAllowed) => (
- {
- if (currentOrg?.scimEnabled) {
- createNotification({
- text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
- type: "error"
- });
- return;
+ {userId !== membership.user.id && (
+
+
+
+
+
+
+
+
+
+
+ {(isAllowed) => (
+
+ handlePopUpOpen("orgMembership", {
+ membershipId: membership.id,
+ role: membership.role
+ })
}
-
- if (!membership.isActive) {
- // activate user
- await updateOrgMembership({
- organizationId: orgId,
- membershipId,
- isActive: true
- });
-
- return;
+ disabled={!isAllowed}
+ >
+ Edit User
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ if (currentOrg?.scimEnabled) {
+ createNotification({
+ text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
+ type: "error"
+ });
+ return;
+ }
- // deactivate user
- handlePopUpOpen("deactivateMember", {
- orgMembershipId: membershipId,
- username: membership.user.username
- });
- }}
- disabled={!isAllowed}
- >
- {`${membership.isActive ? "Deactivate" : "Activate"} User`}
-
- )}
-
-
- {(isAllowed) => (
- {
- if (currentOrg?.scimEnabled) {
- createNotification({
- text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
- type: "error"
+ if (!membership.isActive) {
+ // activate user
+ await updateOrgMembership({
+ organizationId: orgId,
+ membershipId,
+ isActive: true
+ });
+
+ return;
+ }
+
+ // deactivate user
+ handlePopUpOpen("deactivateMember", {
+ orgMembershipId: membershipId,
+ username: membership.user.username
});
- return;
- }
+ }}
+ disabled={!isAllowed}
+ >
+ {`${membership.isActive ? "Deactivate" : "Activate"} User`}
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ if (currentOrg?.scimEnabled) {
+ createNotification({
+ text: "You cannot manage users from Infisical when SCIM is enabled for your organization",
+ type: "error"
+ });
+ return;
+ }
- handlePopUpOpen("removeMember", {
- orgMembershipId: membershipId,
- username: membership.user.username
- });
- }}
- disabled={!isAllowed}
- >
- Remove User
-
- )}
-
-
-
+ handlePopUpOpen("removeMember", {
+ orgMembershipId: membershipId,
+ username: membership.user.username
+ });
+ }}
+ disabled={!isAllowed}
+ >
+ Remove User
+
+ )}
+
+
+
+ )}
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
index ffd118abf..dabc97c5f 100644
--- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, IconButton } from "@app/components/v2";
-import { useOrganization,useUser } from "@app/context";
+import { useOrganization, useUser } from "@app/context";
import { useDeleteUserFromWorkspace, useGetOrgMembership } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -51,7 +51,7 @@ export const UserProjectsSection = ({ membershipId }: Props) => {
Projects
- {userId !== membership.user.id && (
+ {userId !== membership.user.id && Boolean(membership.user.publicKey) && (
Date: Thu, 18 Jul 2024 20:46:36 +0700
Subject: [PATCH 25/30] Public key to not invited state change for project
provisioning ui
---
.../components/UserProjectsSection/UserProjectsSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
index dabc97c5f..c93e1fa8b 100644
--- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
+++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserProjectsSection.tsx
@@ -51,7 +51,7 @@ export const UserProjectsSection = ({ membershipId }: Props) => {
Projects
- {userId !== membership.user.id && Boolean(membership.user.publicKey) && (
+ {userId !== membership.user.id && membership.status !== "invited" && (
Date: Thu, 18 Jul 2024 16:23:09 +0200
Subject: [PATCH 26/30] style: change actions to elipses dropdown
---
.../components/ApprovalPolicyRow.tsx | 86 +++++++++++--------
1 file changed, 51 insertions(+), 35 deletions(-)
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
index 3aa5ce486..cdfce0a45 100644
--- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx
@@ -1,6 +1,7 @@
import { useState } from "react";
-import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { faCheckCircle, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -9,7 +10,6 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
- IconButton,
Input,
Td,
Tr
@@ -145,39 +145,55 @@ export const ApprovalPolicyRow = ({
-
-
- {(isAllowed) => (
-
-
-
- )}
-
-
- {(isAllowed) => (
-
-
-
- )}
-
-
+
+
+
+
+
+
+
+
+ {(isAllowed) => (
+ {
+ e.stopPropagation();
+ onEdit();
+ }}
+ disabled={!isAllowed}
+ >
+ Edit Policy
+
+ )}
+
+
+ {(isAllowed) => (
+ {
+ e.stopPropagation();
+ onDelete();
+ }}
+ disabled={!isAllowed}
+ >
+ Delete Policy
+
+ )}
+
+
+
|
);
From ecb5342a554e1eb32932325b1dfaaa4f2ac02948 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 16:23:56 +0200
Subject: [PATCH 27/30] fix(backend): ensure idempotent migration
---
..._add-enforcement-level-secrets-policies.ts | 20 +++++++++++++------
...8_add-enforcement-level-access-policies.ts | 20 +++++++++++++------
2 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts
index 4a1df888a..ee668d145 100644
--- a/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts
+++ b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts
@@ -2,14 +2,22 @@ import { Knex } from "knex";
import { EnforcementLevel } from "@app/lib/types";
+import { TableName } from "../schemas";
+
export async function up(knex: Knex): Promise {
- await knex.schema.table("secret_approval_policies", (table) => {
- table.specificType("enforcementLevel", "VARCHAR(10)").notNullable().defaultTo(EnforcementLevel.Hard);
- });
+ const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "enforcementLevel");
+ if (!hasColumn) {
+ await knex.schema.table(TableName.SecretApprovalPolicy, (table) => {
+ table.string("enforcementLevel", 10).notNullable().defaultTo(EnforcementLevel.Hard);
+ });
+ }
}
export async function down(knex: Knex): Promise {
- await knex.schema.table("secret_approval_policies", (table) => {
- table.dropColumn("enforcementLevel");
- });
+ const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "enforcementLevel");
+ if (hasColumn) {
+ await knex.schema.table(TableName.SecretApprovalPolicy, (table) => {
+ table.dropColumn("enforcementLevel");
+ });
+ }
}
diff --git a/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts
index 9aeb8d51e..83ef07201 100644
--- a/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts
+++ b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts
@@ -2,14 +2,22 @@ import { Knex } from "knex";
import { EnforcementLevel } from "@app/lib/types";
+import { TableName } from "../schemas";
+
export async function up(knex: Knex): Promise {
- await knex.schema.table("access_approval_policies", (table) => {
- table.specificType("enforcementLevel", "VARCHAR(10)").notNullable().defaultTo(EnforcementLevel.Hard);
- });
+ const hasColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "enforcementLevel");
+ if (!hasColumn) {
+ await knex.schema.table(TableName.AccessApprovalPolicy, (table) => {
+ table.string("enforcementLevel", 10).notNullable().defaultTo(EnforcementLevel.Hard);
+ });
+ }
}
export async function down(knex: Knex): Promise {
- await knex.schema.table("access_approval_policies", (table) => {
- table.dropColumn("enforcementLevel");
- });
+ const hasColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "enforcementLevel");
+ if (hasColumn) {
+ await knex.schema.table(TableName.AccessApprovalPolicy, (table) => {
+ table.dropColumn("enforcementLevel");
+ });
+ }
}
From 53c64b759cf54195e572fe6440446132476351d7 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 17:37:12 +0200
Subject: [PATCH 28/30] feat(frontend): adding tooltip for labels
---
.../components/v2/FormControl/FormControl.tsx | 23 +++++++++++++++----
.../components/AccessPolicyModal.tsx | 11 +--------
2 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx
index 35f85383d..9711e3e68 100644
--- a/frontend/src/components/v2/FormControl/FormControl.tsx
+++ b/frontend/src/components/v2/FormControl/FormControl.tsx
@@ -1,9 +1,11 @@
import { cloneElement, ReactNode } from "react";
-import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
+import { faExclamationTriangle, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Label from "@radix-ui/react-label";
import { twMerge } from "tailwind-merge";
+import { Tooltip } from "../Tooltip";
+
export type FormLabelProps = {
id?: string;
isRequired?: boolean;
@@ -11,9 +13,10 @@ export type FormLabelProps = {
label?: ReactNode;
icon?: ReactNode;
className?: string;
+ tooltipText?: string;
};
-export const FormLabel = ({ id, label, isRequired, icon, className,isOptional }: FormLabelProps) => (
+export const FormLabel = ({ id, label, isRequired, icon, className,isOptional, tooltipText }: FormLabelProps) => (
*}
{isOptional && - Optional}
- {icon && (
+ {icon && !tooltipText && (
{icon}
)}
+ {tooltipText && (
+
+
+
+ )}
);
@@ -64,6 +76,7 @@ export type FormControlProps = {
children: JSX.Element;
className?: string;
icon?: ReactNode;
+ tooltipText?: string;
};
export const FormControl = ({
@@ -76,7 +89,8 @@ export const FormControl = ({
id,
isError,
icon,
- className
+ className,
+ tooltipText
}: FormControlProps): JSX.Element => {
return (
@@ -87,6 +101,7 @@ export const FormControl = ({
isRequired={isRequired}
id={id}
icon={icon}
+ tooltipText={tooltipText}
/>
) : (
label
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
index 54d88ed08..4be290827 100644
--- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
@@ -7,8 +7,6 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
- Alert,
- AlertDescription,
Button,
DropdownMenu,
DropdownMenuContent,
@@ -94,7 +92,6 @@ export const AccessPolicyForm = ({
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
- const enforcementLevel = watch("enforcementLevel");
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
const handleCreatePolicy = async (data: TFormSchema) => {
@@ -330,6 +327,7 @@ export const AccessPolicyForm = ({
label="Enforcement Level"
isError={Boolean(error)}
errorText={error?.message}
+ tooltipText="Choose 'Hard' for required approval or 'Soft' for optional approval"
helperText={
field.value === EnforcementLevel.Hard
? "Hard enforcement prevents any request from being deployed without prior approval"
@@ -352,13 +350,6 @@ export const AccessPolicyForm = ({
)}
/>
- {enforcementLevel === EnforcementLevel.Soft && (
-
-
- Soft enforcement allows requesters to bypass approval, which may reduce system security and stability.
-
-
- )}
diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
index 4be290827..c9eb61185 100644
--- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx
@@ -178,11 +178,13 @@ export const AccessPolicyForm = ({
control={control}
name="policyType"
defaultValue={PolicyType.ChangePolicy}
+
render={({ field: { value, onChange }, fieldState: { error } }) => (
From bc5003ae4c0fec8e5db62c9b20c0b89a42bcbc26 Mon Sep 17 00:00:00 2001
From: Alfonso Hernandez
Date: Thu, 18 Jul 2024 18:53:45 +0200
Subject: [PATCH 30/30] style(frontend): type bypass
---
.../AccessApprovalRequest/components/ReviewAccessModal.tsx | 2 +-
.../components/SecretApprovalRequestAction.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
index c61f94241..1b8e17593 100644
--- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx
@@ -172,7 +172,7 @@ export const ReviewAccessRequestModal = ({
className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
>
- Approve without waiting for requirements to be met (by pass policy protection)
+ Approve without waiting for requirements to be met (bypass policy protection)
diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
index 95d67fc3a..f3b9d7b1e 100644
--- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
+++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx
@@ -114,7 +114,7 @@ export const SecretApprovalRequestAction = ({
className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
>
- Merge without waiting for approval (by pass secret change policy)
+ Merge without waiting for approval (bypass secret change policy)