feat:simplified permission service

This commit is contained in:
=
2025-10-09 23:58:09 +05:30
parent 6dca5b3fa2
commit c6cab7a528
5 changed files with 575 additions and 1418 deletions
@@ -1,17 +1,15 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
import { Knex } from "knex";
import {
AccessScope,
ActionProjectType,
OrgMembershipRole,
ProjectMembershipRole,
ServiceTokenScopes,
TIdentityProjectMemberships,
TProjectMemberships
ServiceTokenScopes
} from "@app/db/schemas";
import {
cryptographicOperatorPermissions,
@@ -25,27 +23,81 @@ import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/
import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
import { escapeHandlebarsMissingDict } from "./permission-fns";
import {
TBuildOrgPermissionDTO,
TBuildProjectPermissionDTO,
TGetIdentityProjectPermissionArg,
TGetProjectPermissionArg,
TGetServiceTokenProjectPermissionArg,
TGetUserProjectPermissionArg,
TPermissionServiceFactory
} from "./permission-service-types";
import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission";
const buildOrgPermissionRules = (orgUserRoles: TBuildOrgPermissionDTO) => {
const rules = orgUserRoles
.map(({ role, permissions }) => {
switch (role) {
case OrgMembershipRole.Admin:
return orgAdminPermissions;
case OrgMembershipRole.Member:
return orgMemberPermissions;
case OrgMembershipRole.NoAccess:
return orgNoAccessPermissions;
case OrgMembershipRole.Custom:
return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
);
default:
throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` });
}
})
.reduce((prev, curr) => prev.concat(curr), []);
return rules;
};
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles
.map(({ role, permissions }) => {
switch (role) {
case ProjectMembershipRole.Admin:
return projectAdminPermissions;
case ProjectMembershipRole.Member:
return projectMemberPermissions;
case ProjectMembershipRole.Viewer:
return projectViewerPermission;
case ProjectMembershipRole.NoAccess:
return projectNoAccessPermissions;
case ProjectMembershipRole.SshHostBootstrapper:
return sshHostBootstrapPermissions;
case ProjectMembershipRole.KmsCryptographicOperator:
return cryptographicOperatorPermissions;
case ProjectMembershipRole.Custom: {
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
);
}
default:
throw new NotFoundError({
name: "ProjectRoleInvalid",
message: `Project role '${role}' not found`
});
}
})
.reduce((prev, curr) => prev.concat(curr), []);
return rules;
};
type TPermissionServiceFactoryDep = {
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
@@ -53,6 +105,8 @@ type TPermissionServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">;
permissionDAL: TPermissionDALFactory;
keyStore: TKeyStoreFactory;
userDAL: Pick<TUserDALFactory, "findById">;
identityDAL: Pick<TIdentityDALFactory, "findById">;
};
export const permissionServiceFactory = ({
@@ -61,33 +115,10 @@ export const permissionServiceFactory = ({
projectRoleDAL,
serviceTokenDAL,
projectDAL,
userDAL,
identityDAL,
keyStore
}: TPermissionServiceFactoryDep): TPermissionServiceFactory => {
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
const rules = orgUserRoles
.map(({ role, permissions }) => {
switch (role) {
case OrgMembershipRole.Admin:
return orgAdminPermissions;
case OrgMembershipRole.Member:
return orgMemberPermissions;
case OrgMembershipRole.NoAccess:
return orgNoAccessPermissions;
case OrgMembershipRole.Custom:
return unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
);
default:
throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` });
}
})
.reduce((prev, curr) => prev.concat(curr), []);
return createMongoAbility<OrgPermissionSet>(rules, {
conditionsMatcher
});
};
const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => {
const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId);
await keyStore.pgIncrementBy(projectPermissionDalVersionKey, {
@@ -97,147 +128,59 @@ export const permissionServiceFactory = ({
});
};
const calculateProjectPermissionTtl = (membership: unknown): number => {
const now = new Date();
let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds;
const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => {
return items
.filter((item) => item.isTemporary && item.temporaryAccessEndTime)
.map((item) => item.temporaryAccessEndTime!)
.filter((endTime) => endTime > now)
.reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null);
};
const roleTimes: Date[] = [];
const additionalPrivilegeTimes: Date[] = [];
if (
membership &&
typeof membership === "object" &&
"roles" in membership &&
Array.isArray((membership as Record<string, unknown>).roles)
) {
const roles = (membership as Record<string, unknown>).roles as Array<{
temporaryAccessEndTime?: Date | null;
isTemporary?: boolean;
}>;
const minRoleEndTime = getMinEndTime(roles);
if (minRoleEndTime) roleTimes.push(minRoleEndTime);
}
if (
membership &&
typeof membership === "object" &&
"additionalPrivileges" in membership &&
Array.isArray((membership as Record<string, unknown>).additionalPrivileges)
) {
const additionalPrivileges = (membership as Record<string, unknown>).additionalPrivileges as Array<{
temporaryAccessEndTime?: Date | null;
isTemporary?: boolean;
}>;
const minAdditionalEndTime = getMinEndTime(additionalPrivileges);
if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime);
}
const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes];
if (allEndTimes.length > 0) {
const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min));
const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000);
minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry));
}
return minTtl;
};
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles
.map(({ role, permissions }) => {
switch (role) {
case ProjectMembershipRole.Admin:
return projectAdminPermissions;
case ProjectMembershipRole.Member:
return projectMemberPermissions;
case ProjectMembershipRole.Viewer:
return projectViewerPermission;
case ProjectMembershipRole.NoAccess:
return projectNoAccessPermissions;
case ProjectMembershipRole.SshHostBootstrapper:
return sshHostBootstrapPermissions;
case ProjectMembershipRole.KmsCryptographicOperator:
return cryptographicOperatorPermissions;
case ProjectMembershipRole.Custom: {
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
);
}
default:
throw new NotFoundError({
name: "ProjectRoleInvalid",
message: `Project role '${role}' not found`
});
}
})
.reduce((prev, curr) => prev.concat(curr), []);
return rules;
};
/*
* Get user permission in an organization
*/
const getUserOrgPermission: TPermissionServiceFactory["getUserOrgPermission"] = async (
userId,
orgId,
authMethod,
userOrgId
) => {
// when token is scoped, ensure the passed org id is same as user org id
if (userOrgId && userOrgId !== orgId)
throw new ForbiddenRequestError({ message: "Invalid user token. Scoped to different organization." });
const membership = await permissionDAL.getOrgPermission(userId, orgId);
if (!membership) throw new ForbiddenRequestError({ name: "You are not apart of this organization" });
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new BadRequestError({ name: "Custom organization permission not found" });
}
// If the org ID is API_KEY, the request is being made with an API Key.
// Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization.
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
}
validateOrgSSO(
authMethod,
membership.orgAuthEnforced,
membership.orgGoogleSsoAuthEnforced,
membership.bypassOrgAuthEnabled,
membership.role as OrgMembershipRole
);
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
membership?.groups?.map(({ role, customRolePermission }) => ({
role,
permissions: customRolePermission
})) || []
);
return { permission: buildOrgPermission(finalPolicyRoles), membership };
};
const getIdentityOrgPermission = async (identityId: string, orgId: string) => {
const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId);
if (!membership) throw new ForbiddenRequestError({ name: "Identity is not apart of this organization" });
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
throw new NotFoundError({ name: `Custom organization permission not found for identity ${identityId}` });
}
return {
permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]),
membership
};
};
// akhilmdhh: will bring this up later
// const calculateProjectPermissionTtl = (membership: unknown): number => {
// const now = new Date();
// let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds;
//
// const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => {
// return items
// .filter((item) => item.isTemporary && item.temporaryAccessEndTime)
// .map((item) => item.temporaryAccessEndTime!)
// .filter((endTime) => endTime > now)
// .reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null);
// };
//
// const roleTimes: Date[] = [];
// const additionalPrivilegeTimes: Date[] = [];
//
// if (
// membership &&
// typeof membership === "object" &&
// "roles" in membership &&
// Array.isArray((membership as Record<string, unknown>).roles)
// ) {
// const roles = (membership as Record<string, unknown>).roles as Array<{
// temporaryAccessEndTime?: Date | null;
// isTemporary?: boolean;
// }>;
// const minRoleEndTime = getMinEndTime(roles);
// if (minRoleEndTime) roleTimes.push(minRoleEndTime);
// }
//
// if (
// membership &&
// typeof membership === "object" &&
// "additionalPrivileges" in membership &&
// Array.isArray((membership as Record<string, unknown>).additionalPrivileges)
// ) {
// const additionalPrivileges = (membership as Record<string, unknown>).additionalPrivileges as Array<{
// temporaryAccessEndTime?: Date | null;
// isTemporary?: boolean;
// }>;
// const minAdditionalEndTime = getMinEndTime(additionalPrivileges);
// if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime);
// }
//
// const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes];
// if (allEndTimes.length > 0) {
// const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min));
// const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000);
// minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry));
// }
//
// return minTtl;
// };
const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async (
type,
@@ -246,17 +189,60 @@ export const permissionServiceFactory = ({
authMethod,
actorOrgId
) => {
switch (type) {
case ActorType.USER:
return getUserOrgPermission(id, orgId, authMethod, actorOrgId);
case ActorType.IDENTITY:
return getIdentityOrgPermission(id, orgId);
default:
throw new BadRequestError({
message: "Invalid actor provided",
name: "Get org permission"
});
if (type !== ActorType.USER && type !== ActorType.IDENTITY) {
throw new BadRequestError({
message: "Invalid actor provided",
name: "Get org permission"
});
}
if (orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
}
const permissionData = await permissionDAL.getPermission({
scopeData: {
scope: AccessScope.Organization,
orgId
},
actorId: id,
actorType: type
});
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" });
const permissionFromRoles = permissionData.flatMap((membership) => {
const activeRoles = membership?.roles
.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
)
.map(({ role, permissions }) => ({ role, permissions }));
const activeAdditionalPrivileges = membership?.additionalPrivileges
.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
)
.map(({ permissions }) => ({ role: OrgMembershipRole.Custom, permissions }));
return activeRoles.concat(activeAdditionalPrivileges);
});
const permission = createMongoAbility<OrgPermissionSet>(buildOrgPermissionRules(permissionFromRoles), {
conditionsMatcher
});
// TODO(simp): validate this
// validateOrgSSO(
// authMethod,
// membership.orgAuthEnforced,
// membership.orgGoogleSsoAuthEnforced,
// membership.bypassOrgAuthEnabled,
// membership.role as OrgMembershipRole
// );
return {
permission,
memberships: permissionData
};
};
// instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like
@@ -270,175 +256,19 @@ export const permissionServiceFactory = ({
message: `Specified role '${role}' was not found in the organization with ID '${orgId}'`
});
return {
permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
permission: createMongoAbility<OrgPermissionSet>(
buildOrgPermissionRules([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]),
{
conditionsMatcher
}
),
role: orgRole
};
}
return { permission: buildOrgPermission([{ role, permissions: [] }]) };
};
// user permission for a project in an organization
const getUserProjectPermission = async ({
userId,
projectId,
authMethod,
userOrgId,
actionProjectType
}: TGetUserProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.USER>> => {
const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId);
if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" });
if (
userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)
) {
throw new NotFoundError({ name: "The permission was not found" });
}
// If the org ID is API_KEY, the request is being made with an API Key.
// Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization.
// Extra: This means that when users are using API keys to make requests, they can't use slug-based routes.
// Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization.
if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
}
validateOrgSSO(
authMethod,
userProjectPermission.orgAuthEnforced,
userProjectPermission.orgGoogleSsoAuthEnforced,
userProjectPermission.bypassOrgAuthEnabled,
userProjectPermission.orgRole
);
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
// join two permissions and pass to build the final permission set
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 unescapedMetadata = objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata });
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return {
permission,
membership: userProjectPermission,
hasRole: (role: string) =>
userProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
};
};
const getIdentityProjectPermission = async ({
identityId,
projectId,
identityOrgId,
actionProjectType
}: TGetIdentityProjectPermissionArg): Promise<TProjectPermissionRT<ActorType.IDENTITY>> => {
const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
if (!identityProjectPermission)
throw new ForbiddenRequestError({
name: `Identity is not a member of the specified project with ID '${projectId}'`
});
if (
identityProjectPermission.roles.some(
({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions
)
) {
throw new NotFoundError({ name: "Custom permission not found" });
}
if (identityProjectPermission.orgId !== identityOrgId) {
throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" });
}
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) {
throw new BadRequestError({
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.`
});
}
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 unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
const unescapedMetadata = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const identityAuthInfo =
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
: {};
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo });
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair,
auth: identityAuthInfo
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
permission: createMongoAbility<OrgPermissionSet>(buildOrgPermissionRules([{ role, permissions: [] }]), {
conditionsMatcher
}
);
return {
permission,
membership: identityProjectPermission,
hasRole: (role: string) =>
identityProjectPermission.roles.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
})
};
};
@@ -480,30 +310,149 @@ export const permissionServiceFactory = ({
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
return {
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
membership: {
shouldUseNewPrivilegeSystem: true
}
memberships: [],
hasRole: () => false
};
};
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
? {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: {
shouldUseNewPrivilegeSystem: boolean;
};
hasRole: (arg: string) => boolean;
} // service token doesn't have both membership and roles
: {
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
orgAuthEnforced: boolean | null | undefined;
orgId: string;
roles: Array<{ role: string }>;
shouldUseNewPrivilegeSystem: boolean;
};
hasRole: (role: string) => boolean;
};
const getProjectPermission: TPermissionServiceFactory["getProjectPermission"] = async ({
actor: inputActor,
actorId: inputActorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType
}) => {
let actor = inputActor;
let actorId = inputActorId;
if (actor === ActorType.SERVICE) {
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
});
}
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
if (
assumedPrivilegeDetailsCtx &&
actor === ActorType.USER &&
actorId === assumedPrivilegeDetailsCtx.requesterId &&
projectId === assumedPrivilegeDetailsCtx.projectId
) {
actor = assumedPrivilegeDetailsCtx.actorType;
actorId = assumedPrivilegeDetailsCtx.actorId;
}
if (ActorType.USER !== actor && ActorType.IDENTITY !== actor) {
throw new BadRequestError({
message: "Invalid actor provided",
name: "Get org permission"
});
}
const projectDetails = await projectDAL.findById(projectId);
if (!projectDetails) {
throw new NotFoundError({ message: `Project with ${projectId} not found` });
}
if (projectDetails.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
}
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== projectDetails.type) {
throw new BadRequestError({
message: `The project is of type ${projectDetails.type}. Operations of type ${actionProjectType} are not allowed.`
});
}
const permissionData = await permissionDAL.getPermission({
scopeData: {
scope: AccessScope.Project,
orgId: projectDetails.orgId,
projectId
},
actorId,
actorType: actor
});
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" });
const permissionFromRoles = permissionData.flatMap((membership) => {
const activeRoles = membership?.roles
.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
)
.map(({ role, permissions }) => ({ role, permissions }));
const activeAdditionalPrivileges = membership?.additionalPrivileges
.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
)
.map(({ permissions }) => ({ role: ProjectMembershipRole.Custom, permissions }));
return activeRoles.concat(activeAdditionalPrivileges);
});
// validateOrgSSO(
// authMethod,
// userProjectPermission.orgAuthEnforced,
// userProjectPermission.orgGoogleSsoAuthEnforced,
// userProjectPermission.bypassOrgAuthEnabled,
// userProjectPermission.orgRole
// );
const rules = buildProjectPermissionRules(permissionFromRoles);
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const unescapedMetadata = objectify(
permissionData?.[0]?.metadata,
(i) => i.key,
(i) => i.value
);
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata });
let username = "";
if (actor === ActorType.USER) {
const userDetails = await userDAL.findById(actorId);
username = userDetails?.username;
} else {
const identityDetails = await identityDAL.findById(actorId);
username = identityDetails?.name;
}
const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
const identityAuthInfo =
unescapedIdentityAuthInfo?.identityId === actorId && unescapedIdentityAuthInfo
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
: {};
const interpolateRules = templatedRules(
{
identity: {
id: actorId,
username,
metadata: metadataKeyValuePair,
auth: identityAuthInfo
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return {
permission,
memberships: permissionData,
hasRole: (role: string) =>
permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role)))
};
};
const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId) => {
// fetch user permissions
@@ -623,117 +572,6 @@ export const permissionServiceFactory = ({
};
};
const getProjectPermission = async <T extends ActorType>({
actor: inputActor,
actorId: inputActorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
let actor = inputActor;
let actorId = inputActorId;
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
if (
assumedPrivilegeDetailsCtx &&
actor === ActorType.USER &&
actorId === assumedPrivilegeDetailsCtx.requesterId &&
projectId === assumedPrivilegeDetailsCtx.projectId
) {
actor = assumedPrivilegeDetailsCtx.actorType;
actorId = assumedPrivilegeDetailsCtx.actorId;
}
if (actor === ActorType.SERVICE) {
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
}
const cachedProjectPermissionVersion = await keyStore.pgGetIntItem(
KeyStorePrefixes.ProjectPermissionDalVersion(projectId)
);
const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0);
const cacheKey = KeyStorePrefixes.ProjectPermission(
projectId,
projectPermissionVersion,
actor,
actorId,
actionProjectType || ActionProjectType.Any
);
try {
const cachedData = await keyStore.getItem(cacheKey);
if (cachedData) {
const parsed = JSON.parse(cachedData) as {
rules: RawRuleOf<MongoAbility<ProjectPermissionSet>>[];
membership: {
roles?: Array<{ role: string; customRoleSlug?: string }>;
[key: string]: unknown;
};
};
const permission = createMongoAbility<ProjectPermissionSet>(parsed.rules, {
conditionsMatcher
});
return {
permission,
membership: parsed.membership,
hasRole: (role: string) =>
parsed.membership.roles?.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
} as TProjectPermissionRT<T>;
}
} catch (error) {
logger.error(error, "Failed to get project permission");
}
let result: TProjectPermissionRT<T>;
switch (actor) {
case ActorType.USER:
result = (await getUserProjectPermission({
userId: actorId,
projectId,
authMethod: actorAuthMethod,
userOrgId: actorOrgId,
actionProjectType
})) as TProjectPermissionRT<T>;
break;
case ActorType.IDENTITY:
result = (await getIdentityProjectPermission({
identityId: actorId,
projectId,
identityOrgId: actorOrgId,
actionProjectType
})) as TProjectPermissionRT<T>;
break;
default:
throw new BadRequestError({
message: "Invalid actor provided",
name: "Get project permission"
});
}
try {
const cacheData = {
rules: result.permission.rules,
membership: result.membership
};
const ttl = calculateProjectPermissionTtl(result.membership);
await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData));
} catch (error) {
logger.error(error, "Failed to cache project permission");
}
return result;
};
const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async (
role,
projectId
@@ -785,15 +623,11 @@ export const permissionServiceFactory = ({
};
return {
getUserOrgPermission,
getOrgPermission,
getUserProjectPermission,
getProjectPermission,
getProjectPermissions,
getOrgPermissionByRole,
getProjectPermissionByRole,
buildOrgPermission,
buildProjectPermissionRules,
checkGroupProjectPermission,
invalidateProjectPermissionCache
};