mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 00:27:30 +00:00
feat(server): completed user additional privilege services
This commit is contained in:
@@ -56,6 +56,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
`${TableName.ProjectUserMembershipRole}.customRoleId`,
|
||||||
`${TableName.ProjectRoles}.id`
|
`${TableName.ProjectRoles}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectUserAdditionalPrivilege,
|
||||||
|
`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
|
||||||
|
`${TableName.ProjectMembership}.id`
|
||||||
|
)
|
||||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
.where("userId", userId)
|
.where("userId", userId)
|
||||||
@@ -69,9 +74,22 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||||
)
|
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||||
.select("permissions");
|
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
|
||||||
|
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
|
||||||
|
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
|
||||||
|
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userApTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
|
.as("userApTemporaryAccessEndTime")
|
||||||
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
@@ -102,15 +120,44 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
customRoleSlug: z.string().optional().nullable()
|
customRoleSlug: z.string().optional().nullable()
|
||||||
}).parse(data)
|
}).parse(data)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "userApId",
|
||||||
|
label: "additionalPrivileges" as const,
|
||||||
|
mapper: ({
|
||||||
|
userApId,
|
||||||
|
userApPermissions,
|
||||||
|
userApIsTemporary,
|
||||||
|
userApTemporaryMode,
|
||||||
|
userApTemporaryRange,
|
||||||
|
userApTemporaryAccessEndTime,
|
||||||
|
userApTemporaryAccessStartTime
|
||||||
|
}) => ({
|
||||||
|
id: userApId,
|
||||||
|
permissions: userApPermissions,
|
||||||
|
temporaryRange: userApTemporaryRange,
|
||||||
|
temporaryMode: userApTemporaryMode,
|
||||||
|
temporaryAccessEndTime: userApTemporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime: userApTemporaryAccessStartTime,
|
||||||
|
isTemporary: userApIsTemporary
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
// when introducting cron mode change it here
|
// when introducting cron mode change it here
|
||||||
const activeRoles = permission?.[0]?.roles.filter(
|
const activeRoles = permission?.[0]?.roles?.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
);
|
);
|
||||||
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
|
||||||
|
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
|
||||||
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
|
);
|
||||||
|
|
||||||
|
return permission?.[0]
|
||||||
|
? { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }
|
||||||
|
: undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,9 +198,17 @@ export const permissionServiceFactory = ({
|
|||||||
|
|
||||||
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
validateOrgSAML(authMethod, membership.orgAuthEnforced);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(membership.roles),
|
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||||
membership,
|
membership: userProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
membership.roles.findIndex(({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug) !== -1
|
membership.roles.findIndex(({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug) !== -1
|
||||||
};
|
};
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectUserAdditionalPrivilegeDALFactory = ReturnType<typeof projectUserAdditionalPrivilegeDALFactory>;
|
||||||
|
|
||||||
|
export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
|
||||||
|
import {
|
||||||
|
ProjectUserAdditionalPrivilegeTemporaryMode,
|
||||||
|
TCreateUserPrivilegeDTO,
|
||||||
|
TDeleteUserPrivilegeDTO,
|
||||||
|
TGetUserPrivilegeDetailsDTO,
|
||||||
|
TUpdateUserPrivilegeDTO
|
||||||
|
} from "./project-user-additional-privilege-types";
|
||||||
|
|
||||||
|
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
|
||||||
|
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
|
||||||
|
typeof projectUserAdditionalPrivilegeServiceFactory
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||||
|
projectUserAdditionalPrivilegeDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
permissionService
|
||||||
|
}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => {
|
||||||
|
const create = async ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
permissions: customPermission,
|
||||||
|
actorOrgId,
|
||||||
|
description,
|
||||||
|
projectMembershipId,
|
||||||
|
...dto
|
||||||
|
}: TCreateUserPrivilegeDTO) => {
|
||||||
|
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
|
||||||
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectMembership.projectId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId });
|
||||||
|
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||||
|
|
||||||
|
if (!dto.isTemporary) {
|
||||||
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||||
|
projectMembershipId,
|
||||||
|
slug,
|
||||||
|
permissions: customPermission,
|
||||||
|
name,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
return additionalPrivilege;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||||
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||||
|
projectMembershipId,
|
||||||
|
slug,
|
||||||
|
permissions: customPermission,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
|
||||||
|
temporaryRange: dto.temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||||
|
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||||
|
});
|
||||||
|
return additionalPrivilege;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateById = async ({ privilegeId, actorOrgId, actor, actorId, ...dto }: TUpdateUserPrivilegeDTO) => {
|
||||||
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
|
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||||
|
|
||||||
|
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||||
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectMembership.projectId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
if (dto?.slug) {
|
||||||
|
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
|
||||||
|
slug: dto.slug,
|
||||||
|
projectMembershipId: projectMembership.id
|
||||||
|
});
|
||||||
|
if (existingSlug && existingSlug.id !== userPrivilege.id)
|
||||||
|
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
|
||||||
|
if (isTemporary) {
|
||||||
|
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
|
||||||
|
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
|
||||||
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
||||||
|
...dto,
|
||||||
|
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||||
|
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||||
|
});
|
||||||
|
return additionalPrivilege;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
||||||
|
...dto,
|
||||||
|
isTemporary: false,
|
||||||
|
temporaryAccessStartTime: null,
|
||||||
|
temporaryAccessEndTime: null,
|
||||||
|
temporaryRange: null,
|
||||||
|
temporaryMode: null
|
||||||
|
});
|
||||||
|
return additionalPrivilege;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = async ({ actorId, actor, actorOrgId, privilegeId }: TDeleteUserPrivilegeDTO) => {
|
||||||
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
|
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||||
|
|
||||||
|
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||||
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectMembership.projectId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
||||||
|
return deletedPrivilege;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrivilegeDetailsById = async ({ privilegeId, actorOrgId, actor, actorId }: TGetUserPrivilegeDetailsDTO) => {
|
||||||
|
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
|
||||||
|
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
|
||||||
|
|
||||||
|
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
|
||||||
|
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectMembership.projectId,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
|
return userPrivilege;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
create,
|
||||||
|
updateById,
|
||||||
|
deleteById,
|
||||||
|
getPrivilegeDetailsById
|
||||||
|
};
|
||||||
|
};
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum ProjectUserAdditionalPrivilegeTemporaryMode {
|
||||||
|
Relative = "relative"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateUserPrivilegeDTO = (
|
||||||
|
| {
|
||||||
|
permissions: unknown;
|
||||||
|
projectMembershipId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
isTemporary: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
permissions: unknown;
|
||||||
|
projectMembershipId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
isTemporary: true;
|
||||||
|
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}
|
||||||
|
) &
|
||||||
|
Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit<TProjectPermission, "projectId"> &
|
||||||
|
Partial<{
|
||||||
|
permissions: unknown;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
|
||||||
|
temporaryRange: string;
|
||||||
|
temporaryAccessStartTime: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type TDeleteUserPrivilegeDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
|
||||||
|
|
||||||
|
export type TGetUserPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
|
||||||
Reference in New Issue
Block a user