From 74b95d92ab2830d753828f27a8df86cf2720677b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 8 Jan 2025 02:48:47 +0800 Subject: [PATCH] feat: backend setup --- backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/routes/v1/secret-router.ts | 71 +++ .../ee/services/permission/permission-dal.ts | 560 +++++++++++++++++- .../services/permission/permission-service.ts | 121 ++++ backend/src/lib/api-docs/constants.ts | 6 + backend/src/services/secret/secret-service.ts | 64 +- backend/src/services/secret/secret-types.ts | 6 + 7 files changed, 827 insertions(+), 3 deletions(-) create mode 100644 backend/src/ee/routes/v1/secret-router.ts diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 6a5ddf51e..5f931440c 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -22,6 +22,7 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router"; import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router"; import { registerSecretRotationRouter } from "./secret-rotation-router"; +import { registerSecretRouter } from "./secret-router"; import { registerSecretScanningRouter } from "./secret-scanning-router"; import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSnapshotRouter } from "./snapshot-router"; @@ -92,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerLdapRouter, { prefix: "/ldap" }); await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" }); await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); + await server.register(registerSecretRouter, { prefix: "/secrets" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerGroupRouter, { prefix: "/groups" }); await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" }); diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts new file mode 100644 index 000000000..74821d180 --- /dev/null +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -0,0 +1,71 @@ +import z from "zod"; + +import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; +import { RAW_SECRETS } from "@app/lib/api-docs"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const AccessListEntrySchema = z + .object({ + allowedActions: z.nativeEnum(ProjectPermissionActions).array(), + id: z.string(), + membershipId: z.string(), + name: z.string() + }) + .array(); + +export const registerSecretRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/raw/:secretName/access-list", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get list of users, machine identities, and groups with access to a secret", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName) + }), + querystring: z.object({ + workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId), + environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(RAW_SECRETS.GET_ACCESS_LIST.secretPath) + }), + response: { + 200: z.object({ + groups: AccessListEntrySchema, + identities: AccessListEntrySchema, + users: AccessListEntrySchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { secretName } = req.params; + const { secretPath, environment, workspaceId: projectId } = req.query; + + return server.services.secret.getSecretAccessList({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + secretPath, + environment, + projectId, + secretName + }); + } + }); +}; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 3a3c82414..ca46442e9 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -125,6 +125,404 @@ export const permissionDALFactory = (db: TDbClient) => { } }; + const getProjectGroupPermissions = async (projectId: string) => { + try { + const docs = await db + .replicaNode()(TableName.GroupProjectMembership) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`) + .join( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .leftJoin( + { groupCustomRoles: TableName.ProjectRoles }, + `${TableName.GroupProjectMembershipRole}.customRoleId`, + `groupCustomRoles.id` + ) + .where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId) + .select( + db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Groups).as("groupId"), + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"), + db + .ref("customRoleId") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleCustomRoleId"), + db + .ref("isTemporary") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleIsTemporary"), + db + .ref("temporaryMode") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryMode"), + db + .ref("temporaryRange") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("groupProjectMembershipRoleTemporaryAccessEndTime") + ); + + const groupPermissions = sqlNestRelationships({ + data: docs, + key: "groupId", + parentMapper: ({ groupId, groupName, membershipId }) => ({ + groupId, + username: groupName, + id: membershipId + }), + childrenMapper: [ + { + key: "groupProjectMembershipRoleId", + label: "groupRoles" as const, + mapper: ({ + groupProjectMembershipRoleId, + groupProjectMembershipRole, + groupProjectMembershipRolePermission, + groupProjectMembershipRoleCustomRoleSlug, + groupProjectMembershipRoleIsTemporary, + groupProjectMembershipRoleTemporaryMode, + groupProjectMembershipRoleTemporaryAccessEndTime, + groupProjectMembershipRoleTemporaryAccessStartTime, + groupProjectMembershipRoleTemporaryRange + }) => ({ + id: groupProjectMembershipRoleId, + role: groupProjectMembershipRole, + customRoleSlug: groupProjectMembershipRoleCustomRoleSlug, + permissions: groupProjectMembershipRolePermission, + temporaryRange: groupProjectMembershipRoleTemporaryRange, + temporaryMode: groupProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: groupProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: groupProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: groupProjectMembershipRoleIsTemporary + }) + } + ] + }); + + return groupPermissions + .map((groupPermission) => { + if (!groupPermission) return undefined; + + const activeGroupRoles = + groupPermission?.groupRoles?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + return { + ...groupPermission, + roles: activeGroupRoles + }; + }) + .filter((item): item is NonNullable => Boolean(item)); + } catch (error) { + throw new DatabaseError({ error, name: "GetProjectGroupPermissions" }); + } + }; + + const getProjectUserPermissions = async (projectId: string) => { + try { + const docs = await db + .replicaNode()(TableName.Users) + .where("isGhost", "=", false) + .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { + void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])); + }) + .leftJoin( + TableName.GroupProjectMembershipRole, + `${TableName.GroupProjectMembershipRole}.projectMembershipId`, + `${TableName.GroupProjectMembership}.id` + ) + .leftJoin( + { groupCustomRoles: TableName.ProjectRoles }, + `${TableName.GroupProjectMembershipRole}.customRoleId`, + `groupCustomRoles.id` + ) + .join(TableName.ProjectMembership, (queryBuilder) => { + void queryBuilder + .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); + }) + .leftJoin( + TableName.ProjectUserMembershipRole, + `${TableName.ProjectUserMembershipRole}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) + .leftJoin( + TableName.ProjectRoles, + `${TableName.ProjectUserMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { + void queryBuilder + .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) + .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); + }) + .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) + .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`); + }) + .select( + db.ref("id").withSchema(TableName.Users).as("userId"), + db.ref("username").withSchema(TableName.Users).as("username"), + // groups specific + db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), + db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), + db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), + db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), + db + .ref("customRoleId") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleCustomRoleId"), + db + .ref("isTemporary") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleIsTemporary"), + db + .ref("temporaryMode") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryMode"), + db + .ref("temporaryRange") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.GroupProjectMembershipRole) + .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), + // user specific + db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), + db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), + db + .ref("temporaryMode") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryMode"), + db + .ref("isTemporary") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserMembershipRole) + .as("userProjectMembershipRoleTemporaryAccessEndTime"), + db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), + db + .ref("permissions") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryMode"), + db + .ref("isTemporary") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryRange"), + db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userAdditionalPrivilegesTemporaryAccessEndTime"), + // general + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("orgId").withSchema(TableName.Project), + db.ref("type").withSchema(TableName.Project).as("projectType"), + db.ref("id").withSchema(TableName.Project).as("projectId") + ); + + const userPermissions = sqlNestRelationships({ + data: docs, + key: "userId", + parentMapper: ({ + orgId, + username, + orgAuthEnforced, + membershipId, + groupMembershipId, + membershipCreatedAt, + groupMembershipCreatedAt, + groupMembershipUpdatedAt, + membershipUpdatedAt, + projectType, + userId + }) => ({ + orgId, + orgAuthEnforced, + userId, + projectId, + username, + projectType, + id: membershipId || groupMembershipId, + createdAt: membershipCreatedAt || groupMembershipCreatedAt, + updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt + }), + childrenMapper: [ + { + key: "userGroupProjectMembershipRoleId", + label: "userGroupRoles" as const, + mapper: ({ + userGroupProjectMembershipRoleId, + userGroupProjectMembershipRole, + userGroupProjectMembershipRolePermission, + userGroupProjectMembershipRoleCustomRoleSlug, + userGroupProjectMembershipRoleIsTemporary, + userGroupProjectMembershipRoleTemporaryMode, + userGroupProjectMembershipRoleTemporaryAccessEndTime, + userGroupProjectMembershipRoleTemporaryAccessStartTime, + userGroupProjectMembershipRoleTemporaryRange + }) => ({ + id: userGroupProjectMembershipRoleId, + role: userGroupProjectMembershipRole, + customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, + permissions: userGroupProjectMembershipRolePermission, + temporaryRange: userGroupProjectMembershipRoleTemporaryRange, + temporaryMode: userGroupProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: userGroupProjectMembershipRoleIsTemporary + }) + }, + { + key: "userProjectMembershipRoleId", + label: "projectMembershipRoles" as const, + mapper: ({ + userProjectMembershipRoleId, + userProjectMembershipRole, + userProjectCustomRolePermission, + userProjectMembershipRoleIsTemporary, + userProjectMembershipRoleTemporaryMode, + userProjectMembershipRoleTemporaryRange, + userProjectMembershipRoleTemporaryAccessEndTime, + userProjectMembershipRoleTemporaryAccessStartTime, + userProjectMembershipRoleCustomRoleSlug + }) => ({ + id: userProjectMembershipRoleId, + role: userProjectMembershipRole, + customRoleSlug: userProjectMembershipRoleCustomRoleSlug, + permissions: userProjectCustomRolePermission, + temporaryRange: userProjectMembershipRoleTemporaryRange, + temporaryMode: userProjectMembershipRoleTemporaryMode, + temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, + isTemporary: userProjectMembershipRoleIsTemporary + }) + }, + { + key: "userAdditionalPrivilegesId", + label: "additionalPrivileges" as const, + mapper: ({ + userAdditionalPrivilegesId, + userAdditionalPrivilegesPermissions, + userAdditionalPrivilegesIsTemporary, + userAdditionalPrivilegesTemporaryMode, + userAdditionalPrivilegesTemporaryRange, + userAdditionalPrivilegesTemporaryAccessEndTime, + userAdditionalPrivilegesTemporaryAccessStartTime + }) => ({ + id: userAdditionalPrivilegesId, + permissions: userAdditionalPrivilegesPermissions, + temporaryRange: userAdditionalPrivilegesTemporaryRange, + temporaryMode: userAdditionalPrivilegesTemporaryMode, + temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, + temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, + isTemporary: userAdditionalPrivilegesIsTemporary + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return userPermissions + .map((userPermission) => { + if (!userPermission) return undefined; + if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined; + + // when introducting cron mode change it here + const activeRoles = + userPermission?.projectMembershipRoles?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + const activeGroupRoles = + userPermission?.userGroupRoles?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + const activeAdditionalPrivileges = + userPermission?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) ?? []; + + return { + ...userPermission, + roles: [...activeRoles, ...activeGroupRoles], + additionalPrivileges: activeAdditionalPrivileges + }; + }) + .filter((item): item is NonNullable => Boolean(item)); + } catch (error) { + throw new DatabaseError({ error, name: "GetProjectUserPermissions" }); + } + }; + const getProjectPermission = async (userId: string, projectId: string) => { try { const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); @@ -414,6 +812,163 @@ export const permissionDALFactory = (db: TDbClient) => { } }; + const getProjectIdentityPermissions = async (projectId: string) => { + try { + const docs = await db + .replicaNode()(TableName.IdentityProjectMembership) + .join( + TableName.IdentityProjectMembershipRole, + `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`) + .leftJoin( + TableName.ProjectRoles, + `${TableName.IdentityProjectMembershipRole}.customRoleId`, + `${TableName.ProjectRoles}.id` + ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) + .join( + // Join the Project table to later select orgId + TableName.Project, + `${TableName.IdentityProjectMembership}.projectId`, + `${TableName.Project}.id` + ) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) + .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) + .select( + db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project + db.ref("type").withSchema(TableName.Project).as("projectType"), + db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles), + db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), + db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessEndTime"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") + ); + + const permissions = sqlNestRelationships({ + data: docs, + key: "identityId", + parentMapper: ({ + membershipId, + membershipCreatedAt, + membershipUpdatedAt, + orgId, + identityName, + projectType, + identityId + }) => ({ + id: membershipId, + identityId, + username: identityName, + projectId, + createdAt: membershipCreatedAt, + updatedAt: membershipUpdatedAt, + orgId, + projectType, + // just a prefilled value + orgAuthEnforced: false + }), + childrenMapper: [ + { + key: "id", + label: "roles" as const, + mapper: (data) => + IdentityProjectMembershipRoleSchema.extend({ + permissions: z.unknown(), + customRoleSlug: z.string().optional().nullable() + }).parse(data) + }, + { + key: "identityApId", + label: "additionalPrivileges" as const, + mapper: ({ + identityApId, + identityApPermissions, + identityApIsTemporary, + identityApTemporaryMode, + identityApTemporaryRange, + identityApTemporaryAccessEndTime, + identityApTemporaryAccessStartTime + }) => ({ + id: identityApId, + permissions: identityApPermissions, + temporaryRange: identityApTemporaryRange, + temporaryMode: identityApTemporaryMode, + temporaryAccessEndTime: identityApTemporaryAccessEndTime, + temporaryAccessStartTime: identityApTemporaryAccessStartTime, + isTemporary: identityApIsTemporary + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return permissions + .map((permission) => { + if (!permission) { + return undefined; + } + + // when introducting cron mode change it here + const activeRoles = permission?.roles.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + const activeAdditionalPrivileges = permission?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + + return { ...permission, roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; + }) + .filter((item): item is NonNullable => Boolean(item)); + } catch (error) { + throw new DatabaseError({ error, name: "GetProjectIdentityPermissions" }); + } + }; + const getProjectIdentityPermission = async (identityId: string, projectId: string) => { try { const docs = await db @@ -568,6 +1123,9 @@ export const permissionDALFactory = (db: TDbClient) => { getOrgPermission, getOrgIdentityPermission, getProjectPermission, - getProjectIdentityPermission + getProjectIdentityPermission, + getProjectUserPermissions, + getProjectIdentityPermissions, + getProjectGroupPermissions }; }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 96e189115..1c79c0a62 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -405,6 +405,126 @@ export const permissionServiceFactory = ({ ForbidOnInvalidProjectType: (type: ProjectType) => void; }; + const getProjectPermissions = async (projectId: string) => { + // fetch user permissions + const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId); + const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => { + const rolePermissions = + userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + + const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const metadataKeyValuePair = escapeHandlebarsMissingMetadata( + objectify( + userProjectPermission.metadata, + (i) => i.key, + (i) => i.value + ) + ); + const interpolateRules = templatedRules( + { + identity: { + id: userProjectPermission.userId, + username: userProjectPermission.username, + metadata: metadataKeyValuePair + } + }, + { data: false } + ); + const permission = createMongoAbility( + JSON.parse(interpolateRules) as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + id: userProjectPermission.userId, + name: userProjectPermission.username, + membershipId: userProjectPermission.id + }; + }); + + // fetch identity permissions + const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId); + const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => { + const rolePermissions = + identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + + const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const metadataKeyValuePair = escapeHandlebarsMissingMetadata( + objectify( + identityProjectPermission.metadata, + (i) => i.key, + (i) => i.value + ) + ); + + const interpolateRules = templatedRules( + { + identity: { + id: identityProjectPermission.identityId, + username: identityProjectPermission.username, + metadata: metadataKeyValuePair + } + }, + { data: false } + ); + const permission = createMongoAbility( + JSON.parse(interpolateRules) as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + id: identityProjectPermission.identityId, + name: identityProjectPermission.username, + membershipId: identityProjectPermission.id + }; + }); + + // fetch group permissions + const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId); + const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => { + const rolePermissions = + groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const rules = buildProjectPermissionRules(rolePermissions); + const permission = createMongoAbility( + rules as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + id: groupProjectPermission.groupId, + name: groupProjectPermission.username, + membershipId: groupProjectPermission.id + }; + }); + + return { + userPermissions, + identityPermissions, + groupPermissions + }; + }; + const getProjectPermission = async ( type: T, id: string, @@ -455,6 +575,7 @@ export const permissionServiceFactory = ({ getOrgPermission, getUserProjectPermission, getProjectPermission, + getProjectPermissions, getOrgPermissionByRole, getProjectPermissionByRole, buildOrgPermission, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 293815d7a..cdc6d6f16 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -741,6 +741,12 @@ export const RAW_SECRETS = { workspaceId: "The ID of the project where the secret is located.", environment: "The slug of the environment where the the secret is located.", secretPath: "The folder path where the secret is located." + }, + GET_ACCESS_LIST: { + secretName: "The name of the secret to get the access list for.", + workspaceId: "The ID of the project where the secret is located.", + environment: "The slug of the environment where the the secret is located.", + secretPath: "The folder path where the secret is located." } } as const; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index fbf90a7f8..99d07354b 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -69,6 +69,7 @@ import { TDeleteSecretRawDTO, TGetASecretDTO, TGetASecretRawDTO, + TGetSecretAccessListDTO, TGetSecretsDTO, TGetSecretsRawDTO, TGetSecretVersionsDTO, @@ -94,7 +95,7 @@ type TSecretServiceFactoryDep = { >; secretV2BridgeService: TSecretV2BridgeServiceFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; - permissionService: Pick; + permissionService: Pick; snapshotService: Pick; secretQueueService: Pick< TSecretQueueFactory, @@ -1147,6 +1148,64 @@ export const secretServiceFactory = ({ return secretV2BridgeService.getSecretReferenceTree(dto); }; + const getSecretAccessList = async (dto: TGetSecretAccessListDTO) => { + const { environment, secretPath, secretName, projectId } = dto; + const secret = await secretV2BridgeService.getSecretByName({ + actor: dto.actor, + actorId: dto.actorId, + actorOrgId: dto.actorOrgId, + actorAuthMethod: dto.actorAuthMethod, + projectId, + secretName, + path: secretPath, + environment, + type: "shared" + }); + + const { userPermissions, identityPermissions, groupPermissions } = await permissionService.getProjectPermissions( + dto.projectId + ); + + const attachAllowedActions = ( + entityPermission: + | (typeof userPermissions)[number] + | (typeof identityPermissions)[number] + | (typeof groupPermissions)[number] + ) => { + const allowedActions = [ + ProjectPermissionActions.Read, + ProjectPermissionActions.Delete, + ProjectPermissionActions.Create, + ProjectPermissionActions.Edit + ].filter((action) => + entityPermission.permission.can( + action, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath, + secretName, + secretTags: secret?.tags?.map((el) => el.slug) + }) + ) + ); + + return { + ...entityPermission, + allowedActions + }; + }; + + const usersWithAccess = userPermissions.map(attachAllowedActions).filter((user) => user.allowedActions.length > 0); + const identitiesWithAccess = identityPermissions + .map(attachAllowedActions) + .filter((identity) => identity.allowedActions.length > 0); + const groupsWithAccess = groupPermissions + .map(attachAllowedActions) + .filter((group) => group.allowedActions.length > 0); + + return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; + }; + const getSecretsRaw = async ({ projectId, path, @@ -2928,6 +2987,7 @@ export const secretServiceFactory = ({ getSecretsCountMultiEnv, getSecretsRawMultiEnv, getSecretReferenceTree, - getSecretsRawByFolderMappings + getSecretsRawByFolderMappings, + getSecretAccessList }; }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index ca5c5a74b..c11cbc034 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -188,6 +188,12 @@ export type TGetSecretsRawDTO = { keys?: string[]; } & TProjectPermission; +export type TGetSecretAccessListDTO = { + environment: string; + secretPath: string; + secretName: string; +} & TProjectPermission; + export type TGetASecretRawDTO = { secretName: string; path: string;