mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-19 21:21:29 +00:00
feat:simplified permission service
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user