import { AbilityBuilder, ForcedSubject, MongoAbility, RawRuleOf, buildMongoQueryMatcher, createMongoAbility } from "@casl/ability"; import { Membership } from "../../models"; import { IRole } from "../models/role"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; import picomatch from "picomatch"; const $glob: FieldInstruction = { type: "field", validate(instruction, value) { if (typeof value !== "string") { throw new Error(`"${instruction.name}" expects value to be a string`); } } }; const glob: JsInterpreter> = (node, object, context) => { const secretPath = context.get(object, node.field); const permissionSecretGlobPath = node.value; return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }); }; export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); export enum ProjectPermissionActions { Read = "read", Create = "create", Edit = "edit", Delete = "delete" } export enum ProjectPermissionSub { Role = "role", Member = "member", Settings = "settings", Integrations = "integrations", Webhooks = "webhooks", ServiceTokens = "service-tokens", Environments = "environments", Tags = "tags", AuditLogs = "audit-logs", IpAllowList = "ip-allowlist", Workspace = "workspace", Secrets = "secrets", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation" } type SubjectFields = { environment: string; secretPath: string; }; export type ProjectPermissionSet = | [ ProjectPermissionActions, ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] | [ProjectPermissionActions, ProjectPermissionSub.Integrations] | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] | [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]; const buildAdminPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Create, ProjectPermissionSub.Member); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Create, ProjectPermissionSub.Role); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace); return build({ conditionsMatcher }); }; export const adminProjectPermissions = buildAdminPermission(); const buildMemberPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Create, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); return build({ conditionsMatcher }); }; export const memberProjectPermissions = buildMemberPermission(); const buildViewerPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); return build({ conditionsMatcher }); }; export const viewerProjectPermission = buildViewerPermission(); export const getUserProjectPermissions = async (userId: string, workspaceId: string) => { // TODO(akhilmhdh): speed this up by pulling from cache later const membership = await Membership.findOne({ user: userId, workspace: workspaceId }) .populate<{ customRole: IRole & { permissions: RawRuleOf>[] }; }>("customRole") .exec(); if (!membership || (membership.role === "custom" && !membership.customRole)) { throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); } if (membership.role === "admin") return { permission: adminProjectPermissions, membership }; if (membership.role === "member") return { permission: memberProjectPermissions, membership }; if (membership.role === "viewer") return { permission: viewerProjectPermission, membership }; if (membership.role === "custom") { const permission = createMongoAbility(membership.customRole.permissions, { conditionsMatcher }); return { permission, membership }; } throw BadRequestError({ message: "User role not found" }); };