feat: fixing ts

This commit is contained in:
=
2025-10-10 00:09:45 +05:30
parent 0ddf98ec15
commit 5a8bad3556
38 changed files with 716 additions and 2437 deletions
-3
View File
@@ -287,9 +287,6 @@ declare module "fastify" {
telemetry: TTelemetryServiceFactory; telemetry: TTelemetryServiceFactory;
dynamicSecret: TDynamicSecretServiceFactory; dynamicSecret: TDynamicSecretServiceFactory;
dynamicSecretLease: TDynamicSecretLeaseServiceFactory; dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
identityProjectAdditionalPrivilegeV2: TIdentityProjectAdditionalPrivilegeV2ServiceFactory;
secretSharing: TSecretSharingServiceFactory; secretSharing: TSecretSharingServiceFactory;
rateLimit: TRateLimitServiceFactory; rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory; userEngagement: TUserEngagementServiceFactory;
@@ -40,7 +40,7 @@ export async function up(knex: Knex): Promise<void> {
t.string("status").defaultTo("invited"); t.string("status").defaultTo("invited");
t.string("inviteEmail"); t.string("inviteEmail");
t.datetime("lastInvitedAt"); t.datetime("lastInvitedAt");
t.datetime("lastLoginAuthMethod"); t.string("lastLoginAuthMethod");
t.datetime("lastLoginTime"); t.datetime("lastLoginTime");
t.specificType("projectFavorites", "text[]"); t.specificType("projectFavorites", "text[]");
t.timestamps(true, true, true); t.timestamps(true, true, true);
+2 -2
View File
@@ -16,11 +16,11 @@ export const MembershipsSchema = z.object({
scopeOrgId: z.string().uuid(), scopeOrgId: z.string().uuid(),
scopeProjectId: z.string().nullable().optional(), scopeProjectId: z.string().nullable().optional(),
scopeNamespaceId: z.string().uuid().nullable().optional(), scopeNamespaceId: z.string().uuid().nullable().optional(),
isActive: z.boolean().nullable().optional(), isActive: z.boolean().default(true),
status: z.string().default("invited").nullable().optional(), status: z.string().default("invited").nullable().optional(),
inviteEmail: z.string().nullable().optional(), inviteEmail: z.string().nullable().optional(),
lastInvitedAt: z.date().nullable().optional(), lastInvitedAt: z.date().nullable().optional(),
lastLoginAuthMethod: z.date().nullable().optional(), lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(), lastLoginTime: z.date().nullable().optional(),
projectFavorites: z.string().array().nullable().optional(), projectFavorites: z.string().array().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
+16 -7
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas";
import { seedData1 } from "../seed-data"; import { seedData1 } from "../seed-data";
export async function seed(knex: Knex): Promise<void> { export async function seed(knex: Knex): Promise<void> {
@@ -24,13 +24,22 @@ export async function seed(knex: Knex): Promise<void> {
]) ])
.returning("*"); .returning("*");
await knex(TableName.OrgMembership).insert([ const [membership] = await knex(TableName.Membership)
.insert([
{
scope: AccessScope.Organization,
scopeOrgId: org.id,
actorUserId: user.id,
isActive: true,
status: OrgMembershipStatus.Accepted
}
])
.returning("*");
await knex(TableName.MembershipRole).insert([
{ {
role: OrgMembershipRole.Admin, membershipId: membership.id,
orgId: org.id, role: OrgMembershipRole.Admin
status: OrgMembershipStatus.Accepted,
userId: user.id,
isActive: true
} }
]); ]);
} }
+35 -23
View File
@@ -6,14 +6,14 @@ import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { initLogger, logger } from "@app/lib/logger"; import { initLogger, logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthMethod } from "@app/services/auth/auth-type"; import { AuthMethod } from "@app/services/auth/auth-type";
import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { userDALFactory } from "@app/services/user/user-dal"; import { userDALFactory } from "@app/services/user/user-dal";
import { import {
AccessScope,
OrgMembershipRole, OrgMembershipRole,
OrgMembershipStatus, OrgMembershipStatus,
ProjectMembershipRole, ProjectMembershipRole,
@@ -23,6 +23,7 @@ import {
TableName TableName
} from "../schemas"; } from "../schemas";
import { seedData1 } from "../seed-data"; import { seedData1 } from "../seed-data";
import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" }, { name: "Development", slug: "dev" },
@@ -39,8 +40,8 @@ const createUserWithGhostUser = async (
) => { ) => {
const projectKeyDAL = projectKeyDALFactory(knex); const projectKeyDAL = projectKeyDALFactory(knex);
const userDAL = userDALFactory(knex); const userDAL = userDALFactory(knex);
const projectMembershipDAL = projectMembershipDALFactory(knex); const membershipDAL = membershipUserDALFactory(knex);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex); const membershipRoleDAL = membershipRoleDALFactory(knex);
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
@@ -63,25 +64,36 @@ const createUserWithGhostUser = async (
.onConflict("userId") .onConflict("userId")
.merge(); .merge();
await knex(TableName.OrgMembership) const [orgMembership] = await knex(TableName.Membership)
.insert({ .insert({
orgId, scope: AccessScope.Organization,
userId: ghostUser.id, scopeOrgId: orgId,
role: OrgMembershipRole.Admin, actorUserId: ghostUser.id,
status: OrgMembershipStatus.Accepted, status: OrgMembershipStatus.Accepted,
isActive: true isActive: true
}) })
.returning("*"); .returning("*");
const [projectMembership] = await knex(TableName.ProjectMembership) await knex(TableName.MembershipRole).insert([
{
membershipId: orgMembership.id,
role: OrgMembershipRole.Admin
}
]);
const [projectMembership] = await knex(TableName.Membership)
.insert({ .insert({
userId: ghostUser.id, actorUserId: ghostUser.id,
projectId scopeProjectId: projectId,
scope: AccessScope.Project,
scopeOrgId: orgId,
status: OrgMembershipStatus.Accepted,
isActive: true
}) })
.returning("*"); .returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({ await knex(TableName.MembershipRole).insert({
projectMembershipId: projectMembership.id, membershipId: projectMembership.id,
role: ProjectMembershipRole.Admin role: ProjectMembershipRole.Admin
}); });
@@ -142,17 +154,16 @@ const createUserWithGhostUser = async (
}); });
// Create a membership for the user // Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create( const userProjectMembership = await membershipDAL.create(
{ {
projectId, scopeProjectId: projectId,
userId: user.id scope: AccessScope.Project,
actorUserId: user.id,
scopeOrgId: orgId
}, },
knex knex
); );
await projectUserMembershipRoleDAL.create( await membershipRoleDAL.create({ membershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, knex);
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
knex
);
// Create a project key for the user // Create a project key for the user
await projectKeyDAL.create( await projectKeyDAL.create(
@@ -195,10 +206,11 @@ export async function seed(knex: Knex): Promise<void> {
}) })
.returning("*"); .returning("*");
const userOrgMembership = await knex(TableName.OrgMembership) const userOrgMembership = await knex(TableName.Membership)
.where({ .where({
orgId: seedData1.organization.id, scopeOrgId: seedData1.organization.id,
userId: seedData1.id actorUserId: seedData1.id,
scope: AccessScope.Organization
}) })
.first(); .first();
+8 -6
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; import { AccessScope, ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas";
import { seedData1 } from "../seed-data"; import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
@@ -23,15 +23,17 @@ export async function seed(knex: Knex): Promise<void> {
}) })
.returning("*"); .returning("*");
const projectMembershipV3 = await knex(TableName.ProjectMembership) const projectMembershipV3 = await knex(TableName.Membership)
.insert({ .insert({
projectId: projectV2.id, scopeProjectId: projectV2.id,
userId: seedData1.id actorUserId: seedData1.id,
scope: AccessScope.Project,
scopeOrgId: seedData1.organization.id
}) })
.returning("*"); .returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({ await knex(TableName.MembershipRole).insert({
role: ProjectMembershipRole.Admin, role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembershipV3[0].id membershipId: projectMembershipV3[0].id
}); });
// create default environments and default folders // create default environments and default folders
+27 -15
View File
@@ -5,13 +5,12 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { initLogger, logger } from "@app/lib/logger"; import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data"; import { seedData1 } from "../seed-data";
export async function seed(knex: Knex): Promise<void> { export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries // Deletes ALL existing entries
await knex(TableName.Identity).del(); await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del();
initLogger(); initLogger();
@@ -78,34 +77,47 @@ export async function seed(knex: Knex): Promise<void> {
isClientSecretRevoked: false isClientSecretRevoked: false
} }
]); ]);
await knex(TableName.IdentityOrgMembership).insert([ const [orgMembership] = await knex(TableName.Membership)
.insert([
{
actorIdentityId: seedData1.machineIdentity.id,
scopeOrgId: seedData1.organization.id,
scope: AccessScope.Organization
}
])
.returning("*");
await knex(TableName.MembershipRole).insert([
{ {
identityId: seedData1.machineIdentity.id, membershipId: orgMembership.id,
orgId: seedData1.organization.id,
role: OrgMembershipRole.Admin role: OrgMembershipRole.Admin
} }
]); ]);
const identityProjectMembership = await knex(TableName.IdentityProjectMembership) const identityProjectMembership = await knex(TableName.Membership)
.insert({ .insert({
identityId: seedData1.machineIdentity.id, actorIdentityId: seedData1.machineIdentity.id,
projectId: seedData1.project.id scopeOrgId: seedData1.organization.id,
scope: AccessScope.Project,
scopeProjectId: seedData1.project.id
}) })
.returning("*"); .returning("*");
await knex(TableName.IdentityProjectMembershipRole).insert({ await knex(TableName.MembershipRole).insert({
role: ProjectMembershipRole.Admin, role: ProjectMembershipRole.Admin,
projectMembershipId: identityProjectMembership[0].id membershipId: identityProjectMembership[0].id
}); });
const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership)
const identityProjectMembershipV3 = await knex(TableName.Membership)
.insert({ .insert({
identityId: seedData1.machineIdentity.id, actorIdentityId: seedData1.machineIdentity.id,
projectId: seedData1.projectV3.id scopeOrgId: seedData1.organization.id,
scope: AccessScope.Project,
scopeProjectId: seedData1.projectV3.id
}) })
.returning("*"); .returning("*");
await knex(TableName.IdentityProjectMembershipRole).insert({ await knex(TableName.MembershipRole).insert({
role: ProjectMembershipRole.Admin, role: ProjectMembershipRole.Admin,
projectMembershipId: identityProjectMembershipV3[0].id membershipId: identityProjectMembershipV3[0].id
}); });
} }
@@ -78,10 +78,7 @@ type TGithubOrgSyncServiceFactoryDep = {
>; >;
groupDAL: Pick<TGroupDALFactory, "insertMany" | "transaction" | "find">; groupDAL: Pick<TGroupDALFactory, "insertMany" | "transaction" | "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgMembershipDAL: Pick< orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOrgMembershipsWithUsersByOrgId">;
TOrgMembershipDALFactory,
"find" | "findOrgMembershipById" | "findOrgMembershipsWithUsersByOrgId"
>;
}; };
export type TGithubOrgSyncServiceFactory = ReturnType<typeof githubOrgSyncServiceFactory>; export type TGithubOrgSyncServiceFactory = ReturnType<typeof githubOrgSyncServiceFactory>;
+25 -1
View File
@@ -195,12 +195,36 @@ export const groupDALFactory = (db: TDbClient) => {
} }
}; };
const findOne = async (filter: Partial<TGroups>, tx?: Knex): Promise<TGroups | undefined> => {
try {
const doc = await (tx || db.replicaNode())(TableName.Groups)
.join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`)
.join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.where((queryBuilder) => {
Object.entries(filter).forEach(([key, value]) => {
void queryBuilder.where(`${TableName.Membership}.${key}`, value);
});
})
.select(
selectAllTableCols(TableName.Groups),
db.ref("role").withSchema(TableName.MembershipRole),
db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole)
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
return { return {
...groupOrm, ...groupOrm,
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupPossibleMembers, findAllGroupPossibleMembers,
findGroupsByProjectId, findGroupsByProjectId,
findById findById,
findOne
}; };
}; };
+26 -19
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { AccessScope, ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
@@ -16,7 +16,7 @@ const addAcceptedUsersToGroup = async ({
group, group,
userGroupMembershipDAL, userGroupMembershipDAL,
userDAL, userDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
@@ -42,13 +42,15 @@ const addAcceptedUsersToGroup = async ({
const projectIds = Array.from( const projectIds = Array.from(
new Set( new Set(
( (
await groupProjectDAL.find( await membershipGroupDAL.find(
{ {
groupId: group.id actorGroupId: group.id,
scopeOrgId: group.orgId,
scope: AccessScope.Project
}, },
{ tx } { tx }
) )
).map((gp) => gp.projectId) ).map((gp) => gp.scopeProjectId as string)
) )
); );
@@ -167,11 +169,11 @@ export const addUsersToGroupByUserIds = async ({
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
tx: outerTx tx: outerTx,
membershipGroupDAL
}: TAddUsersToGroupByUserIds) => { }: TAddUsersToGroupByUserIds) => {
const processAddition = async (tx: Knex) => { const processAddition = async (tx: Knex) => {
const foundMembers = await userDAL.find( const foundMembers = await userDAL.find(
@@ -214,15 +216,18 @@ export const addUsersToGroupByUserIds = async ({
// check if all user(s) are part of the organization // check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership( const existingUserOrgMemberships = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId, [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: group.orgId,
scope: AccessScope.Organization,
$in: { $in: {
[`${TableName.OrgMembership}.userId` as "userId"]: userIds [`${TableName.Membership}.actorUserId` as "actorUserId"]: userIds
} }
}, },
{ tx } { tx }
); );
const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId)); const existingUserOrgMembershipsUserIdsSet = new Set(
existingUserOrgMemberships.map((u) => u.actorUserId as string)
);
userIds.forEach((userId) => { userIds.forEach((userId) => {
if (!existingUserOrgMembershipsUserIdsSet.has(userId)) if (!existingUserOrgMembershipsUserIdsSet.has(userId))
@@ -250,7 +255,7 @@ export const addUsersToGroupByUserIds = async ({
group, group,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
@@ -292,9 +297,9 @@ export const removeUsersFromGroupByUserIds = async ({
userIds, userIds,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
tx: outerTx tx: outerTx,
membershipGroupDAL
}: TRemoveUsersFromGroupByUserIds) => { }: TRemoveUsersFromGroupByUserIds) => {
const processRemoval = async (tx: Knex) => { const processRemoval = async (tx: Knex) => {
const foundMembers = await userDAL.find({ const foundMembers = await userDAL.find({
@@ -352,13 +357,15 @@ export const removeUsersFromGroupByUserIds = async ({
const projectIds = Array.from( const projectIds = Array.from(
new Set( new Set(
( (
await groupProjectDAL.find( await membershipGroupDAL.find(
{ {
groupId: group.id scope: AccessScope.Project,
actorGroupId: group.id,
scopeOrgId: group.orgId
}, },
{ tx } { tx }
) )
).map((gp) => gp.projectId) ).map((gp) => gp.scopeProjectId as string)
) )
); );
@@ -422,11 +429,11 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({
userIds, userIds,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
tx: outerTx tx: outerTx,
membershipGroupDAL
}: TConvertPendingGroupAdditionsToGroupMemberships) => { }: TConvertPendingGroupAdditionsToGroupMemberships) => {
const processConversion = async (tx: Knex) => { const processConversion = async (tx: Knex) => {
const users = await userDAL.find( const users = await userDAL.find(
@@ -463,7 +470,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({
group: pendingGroupAddition.group, group: pendingGroupAddition.group,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
+80 -46
View File
@@ -1,11 +1,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -35,8 +36,9 @@ type TGroupServiceFactoryDep = {
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find" | "findOne" | "create">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers">; membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create" | "delete">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "countAllOrgMembers" | "findById">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
@@ -54,7 +56,6 @@ export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
export const groupServiceFactory = ({ export const groupServiceFactory = ({
userDAL, userDAL,
groupDAL, groupDAL,
groupProjectDAL,
orgDAL, orgDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
projectDAL, projectDAL,
@@ -62,12 +63,14 @@ export const groupServiceFactory = ({
projectKeyDAL, projectKeyDAL,
permissionService, permissionService,
licenseService, licenseService,
oidcConfigDAL oidcConfigDAL,
membershipGroupDAL,
membershipRoleDAL
}: TGroupServiceFactoryDep) => { }: TGroupServiceFactoryDep) => {
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission, membership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -82,25 +85,23 @@ export const groupServiceFactory = ({
message: "Failed to create group due to plan restriction. Upgrade plan to create group." message: "Failed to create group due to plan restriction. Upgrade plan to create group."
}); });
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], actorOrgId);
role, const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId);
actorOrgId const isCustomRole = Boolean(rolePermissionDetails?.role);
);
const isCustomRole = Boolean(customRole);
if (role !== OrgMembershipRole.NoAccess) { if (role !== OrgMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation( const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges, OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups, OrgPermissionSubjects.Groups,
permission, permission,
rolePermission rolePermissionDetails.permission
); );
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({ throw new PermissionBoundaryError({
message: constructPermissionErrorMessage( message: constructPermissionErrorMessage(
"Failed to create group", "Failed to create group",
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges, OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups OrgPermissionSubjects.Groups
), ),
@@ -122,7 +123,25 @@ export const groupServiceFactory = ({
slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`),
orgId: actorOrgId, orgId: actorOrgId,
role: isCustomRole ? OrgMembershipRole.Custom : role, role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id roleId: rolePermissionDetails?.role?.id
},
tx
);
const membership = await membershipGroupDAL.create(
{
actorGroupId: newGroup.id,
scope: AccessScope.Organization,
scopeOrgId: actorOrgId
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role: isCustomRole ? OrgMembershipRole.Custom : role,
customRoleId: rolePermissionDetails?.role?.id
}, },
tx tx
); );
@@ -145,7 +164,7 @@ export const groupServiceFactory = ({
}: TUpdateGroupDTO) => { }: TUpdateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission, membership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -166,32 +185,31 @@ export const groupServiceFactory = ({
throw new NotFoundError({ message: `Failed to find group with ID ${id}` }); throw new NotFoundError({ message: `Failed to find group with ID ${id}` });
} }
let customRole: TOrgRoles | undefined; let customRole: TRoles | undefined;
if (role) { if (role) {
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], group.orgId);
role,
group.orgId const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId);
); const isCustomRole = Boolean(rolePermissionDetails?.role);
const isCustomRole = Boolean(customOrgRole);
const permissionBoundary = validatePrivilegeChangeOperation( const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges, OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups, OrgPermissionSubjects.Groups,
permission, permission,
rolePermission rolePermissionDetails.permission
); );
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({ throw new PermissionBoundaryError({
message: constructPermissionErrorMessage( message: constructPermissionErrorMessage(
"Failed to update group", "Failed to update group",
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.GrantPrivileges, OrgPermissionGroupActions.GrantPrivileges,
OrgPermissionSubjects.Groups OrgPermissionSubjects.Groups
), ),
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
if (isCustomRole) customRole = customOrgRole; if (isCustomRole) customRole = rolePermissionDetails?.role;
} }
const updatedGroup = await groupDAL.transaction(async (tx) => { const updatedGroup = await groupDAL.transaction(async (tx) => {
@@ -211,17 +229,31 @@ export const groupServiceFactory = ({
}, },
{ {
name, name,
slug: slug ? slugify(slug) : undefined, slug: slug ? slugify(slug) : undefined
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id ?? null
}
: {})
}, },
tx tx
); );
if (role) {
const membership = await membershipGroupDAL.findOne(
{
scope: AccessScope.Organization,
actorGroupId: updated.id,
scopeOrgId: updated.orgId
},
tx
);
await membershipRoleDAL.delete({ membershipId: membership.id }, tx);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role: customRole ? OrgMembershipRole.Custom : role,
customRoleId: customRole?.id ?? null
},
tx
);
}
return updated; return updated;
}); });
@@ -341,7 +373,7 @@ export const groupServiceFactory = ({
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission, membership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -373,22 +405,23 @@ export const groupServiceFactory = ({
}); });
} }
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId);
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const permissionBoundary = validatePrivilegeChangeOperation( const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.AddMembers, OrgPermissionGroupActions.AddMembers,
OrgPermissionSubjects.Groups, OrgPermissionSubjects.Groups,
permission, permission,
groupRolePermission rolePermissionDetails.permission
); );
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({ throw new PermissionBoundaryError({
message: constructPermissionErrorMessage( message: constructPermissionErrorMessage(
"Failed to add user to more privileged group", "Failed to add user to more privileged group",
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.AddMembers, OrgPermissionGroupActions.AddMembers,
OrgPermissionSubjects.Groups OrgPermissionSubjects.Groups
), ),
@@ -407,7 +440,7 @@ export const groupServiceFactory = ({
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL projectBotDAL
@@ -431,7 +464,7 @@ export const groupServiceFactory = ({
}: TRemoveUserFromGroupDTO) => { }: TRemoveUserFromGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission, membership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -463,21 +496,22 @@ export const groupServiceFactory = ({
}); });
} }
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId);
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const permissionBoundary = validatePrivilegeChangeOperation( const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.RemoveMembers, OrgPermissionGroupActions.RemoveMembers,
OrgPermissionSubjects.Groups, OrgPermissionSubjects.Groups,
permission, permission,
groupRolePermission rolePermissionDetails.permission
); );
if (!permissionBoundary.isValid) if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({ throw new PermissionBoundaryError({
message: constructPermissionErrorMessage( message: constructPermissionErrorMessage(
"Failed to delete user from more privileged group", "Failed to delete user from more privileged group",
membership.shouldUseNewPrivilegeSystem, shouldUseNewPrivilegeSystem,
OrgPermissionGroupActions.RemoveMembers, OrgPermissionGroupActions.RemoveMembers,
OrgPermissionSubjects.Groups OrgPermissionSubjects.Groups
), ),
@@ -495,7 +529,7 @@ export const groupServiceFactory = ({
userIds: [user.id], userIds: [user.id],
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL projectKeyDAL
}); });
+5 -5
View File
@@ -3,7 +3,7 @@ import { Knex } from "knex";
import { TGroups } from "@app/db/schemas"; import { TGroups } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TGenericPermission } from "@app/lib/types"; import { TGenericPermission } from "@app/lib/types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
@@ -63,7 +63,7 @@ export type TAddUsersToGroup = {
group: TGroups; group: TGroups;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserIdsBatch">; userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserIdsBatch">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
@@ -76,7 +76,7 @@ export type TAddUsersToGroupByUserIds = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
orgDAL: Pick<TOrgDALFactory, "findMembership">; orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
@@ -88,7 +88,7 @@ export type TRemoveUsersFromGroupByUserIds = {
userIds: string[]; userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "transaction">; userDAL: Pick<TUserDALFactory, "find" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
tx?: Knex; tx?: Knex;
}; };
@@ -100,7 +100,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" "find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
@@ -1,12 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityProjectAdditionalPrivilegeV2DALFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeV2DALFactory
>;
export const identityProjectAdditionalPrivilegeV2DALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege);
return orm;
};
@@ -1,434 +0,0 @@
import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal";
import {
IdentityProjectAdditionalPrivilegeTemporaryMode,
TCreateIdentityPrivilegeDTO,
TDeleteIdentityPrivilegeByIdDTO,
TGetIdentityPrivilegeDetailsByIdDTO,
TGetIdentityPrivilegeDetailsBySlugDTO,
TListIdentityPrivilegesDTO,
TUpdateIdentityPrivilegeByIdDTO
} from "./identity-project-additional-privilege-v2-types";
type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
};
export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeV2ServiceFactory
>;
export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
identityProjectAdditionalPrivilegeDAL,
identityProjectDAL,
projectDAL,
permissionService
}: TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep) => {
const create = async ({
slug,
actor,
actorId,
projectId,
actorOrgId,
identityId,
permissions: customPermission,
actorAuthMethod,
...dto
}: TCreateIdentityPrivilegeDTO) => {
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege with provided slug already exists" });
const packedPermission = JSON.stringify(packRules(customPermission));
if (!dto.isTemporary) {
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: packedPermission
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: packedPermission,
isTemporary: true,
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const updateById = async ({
id,
data,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TUpdateIdentityPrivilegeByIdDTO) => {
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id);
if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` });
const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId });
if (!identityProjectMembership)
throw new NotFoundError({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug: data.slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug && existingSlug.id !== identityPrivilege.id)
throw new BadRequestError({ message: "Additional privilege with provided slug already exists" });
}
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined;
if (isTemporary) {
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
slug: data.slug,
permissions: packedPermission,
isTemporary: data.isTemporary,
temporaryRange: data.temporaryRange,
temporaryMode: data.temporaryMode,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
slug: data.slug,
permissions: packedPermission,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const deleteById = async ({ actorId, id, actor, actorOrgId, actorAuthMethod }: TDeleteIdentityPrivilegeByIdDTO) => {
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id);
if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` });
const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId });
if (!identityProjectMembership)
throw new NotFoundError({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
identityRolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
};
const getPrivilegeDetailsById = async ({
id,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetIdentityPrivilegeDetailsByIdDTO) => {
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id);
if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` });
const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId });
if (!identityProjectMembership)
throw new NotFoundError({
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
};
const getPrivilegeDetailsBySlug = async ({
identityId,
slug,
projectSlug,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetIdentityPrivilegeDetailsBySlugDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug ${slug} not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" });
return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
};
const listIdentityProjectPrivileges = async ({
identityId,
actorOrgId,
actor,
actorId,
actorAuthMethod,
projectId
}: TListIdentityPrivilegesDTO) => {
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find(
{
projectMembershipId: identityProjectMembership.id
},
{ sort: [[`${TableName.IdentityProjectAdditionalPrivilege}.slug` as "slug", "asc"]] }
);
return identityPrivileges;
};
return {
getPrivilegeDetailsById,
getPrivilegeDetailsBySlug,
listIdentityProjectPrivileges,
create,
updateById,
deleteById
};
};
@@ -1,55 +0,0 @@
import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission";
export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateIdentityPrivilegeDTO = {
permissions: TProjectPermissionV2Schema[];
identityId: string;
projectId: string;
slug: string;
} & (
| {
isTemporary: false;
}
| {
isTemporary: true;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
) &
Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit<TProjectPermission, "projectId"> & {
data: Partial<{
permissions: TProjectPermissionV2Schema[];
slug: string;
isTemporary: boolean;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}>;
};
export type TDeleteIdentityPrivilegeByIdDTO = Omit<TProjectPermission, "projectId"> & {
id: string;
};
export type TGetIdentityPrivilegeDetailsByIdDTO = Omit<TProjectPermission, "projectId"> & {
id: string;
};
export type TListIdentityPrivilegesDTO = Omit<TProjectPermission, "projectId"> & {
identityId: string;
projectId: string;
};
export type TGetIdentityPrivilegeDetailsBySlugDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
@@ -1,12 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeDALFactory
>;
export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege);
return orm;
};
@@ -1,451 +0,0 @@
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import {
ProjectPermissionIdentityActions,
ProjectPermissionSet,
ProjectPermissionSub
} from "../permission/project-permission";
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
import {
IdentityProjectAdditionalPrivilegeTemporaryMode,
TCreateIdentityPrivilegeDTO,
TDeleteIdentityPrivilegeDTO,
TGetIdentityPrivilegeDetailsDTO,
TListIdentityPrivilegesDTO,
TUpdateIdentityPrivilegeDTO
} from "./identity-project-additional-privilege-types";
type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
};
export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
typeof identityProjectAdditionalPrivilegeServiceFactory
>;
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const identityProjectAdditionalPrivilegeServiceFactory = ({
identityProjectAdditionalPrivilegeDAL,
identityProjectDAL,
permissionService,
projectDAL
}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => {
const create = async ({
slug,
actor,
actorId,
identityId,
projectSlug,
permissions: customPermission,
actorOrgId,
actorAuthMethod,
...dto
}: TCreateIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const packedPermission = JSON.stringify(packRules(customPermission));
if (!dto.isTemporary) {
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: packedPermission
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
projectMembershipId: identityProjectMembership.id,
slug,
permissions: packedPermission,
isTemporary: true,
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const updateBySlug = async ({
projectSlug,
slug,
identityId,
data,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TUpdateIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
targetIdentityPermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) {
throw new NotFoundError({
message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'`
});
}
if (data?.slug) {
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
slug: data.slug,
projectMembershipId: identityProjectMembership.id
});
if (existingSlug && existingSlug.id !== identityPrivilege.id)
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
}
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined;
if (isTemporary) {
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
slug: data.slug,
permissions: packedPermission,
isTemporary: data.isTemporary,
temporaryRange: data.temporaryRange,
temporaryMode: data.temporaryMode,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
slug: data.slug,
permissions: packedPermission,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const deleteBySlug = async ({
actorId,
slug,
identityId,
projectSlug,
actor,
actorOrgId,
actorAuthMethod
}: TDeleteIdentityPrivilegeDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
actor: ActorType.IDENTITY,
actorId: identityProjectMembership.identityId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
identityRolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to edit more privileged identity",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) {
throw new NotFoundError({
message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'`
});
}
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
};
const getPrivilegeDetailsBySlug = async ({
projectSlug,
identityId,
slug,
actorOrgId,
actor,
actorId,
actorAuthMethod
}: TGetIdentityPrivilegeDetailsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug,
projectMembershipId: identityProjectMembership.id
});
if (!identityPrivilege) {
throw new NotFoundError({
message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'`
});
}
return {
...identityPrivilege,
permissions: unpackPermissions(identityPrivilege.permissions)
};
};
const listIdentityProjectPrivileges = async ({
identityId,
actorOrgId,
actor,
actorId,
actorAuthMethod,
projectSlug
}: TListIdentityPrivilegesDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
const projectId = project.id;
const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId });
if (!identityProjectMembership)
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: identityProjectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionIdentityActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
projectMembershipId: identityProjectMembership.id
});
return identityPrivileges.map((el) => ({
...el,
permissions: unpackPermissions(el.permissions)
}));
};
return {
create,
updateBySlug,
deleteBySlug,
getPrivilegeDetailsBySlug,
listIdentityProjectPrivileges
};
};
@@ -1,56 +0,0 @@
import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission";
export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateIdentityPrivilegeDTO = {
permissions: TProjectPermissionV2Schema[];
identityId: string;
projectSlug: string;
slug: string;
} & (
| {
isTemporary: false;
}
| {
isTemporary: true;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
) &
Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit<
TProjectPermission,
"projectId"
> & {
data: Partial<{
permissions: TProjectPermissionV2Schema[];
slug: string;
isTemporary: boolean;
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}>;
};
export type TDeleteIdentityPrivilegeDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
export type TGetIdentityPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
export type TListIdentityPrivilegesDTO = Omit<TProjectPermission, "projectId"> & {
identityId: string;
projectSlug: string;
};
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -12,12 +12,12 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
@@ -49,13 +49,13 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal";
type TLdapConfigServiceFactoryDep = { type TLdapConfigServiceFactoryDep = {
ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne" | "transaction">; ldapConfigDAL: Pick<TLdapConfigDALFactory, "create" | "update" | "findOne" | "transaction">;
ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">; ldapGroupMapDAL: Pick<TLdapGroupMapDALFactory, "find" | "create" | "delete" | "findLdapGroupMapsByLdapConfigId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">; groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
@@ -87,9 +87,9 @@ export const ldapConfigServiceFactory = ({
ldapConfigDAL, ldapConfigDAL,
ldapGroupMapDAL, ldapGroupMapDAL,
orgDAL, orgDAL,
orgMembershipDAL,
groupDAL, groupDAL,
groupProjectDAL, membershipGroupDAL,
membershipRoleDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
@@ -388,25 +388,33 @@ export const ldapConfigServiceFactory = ({
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId, [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgDAL.createMembership( const membership = await orgDAL.createMembership(
{ {
userId: userAlias.userId, actorUserId: userAlias.userId,
orgId, scopeOrgId: orgId,
role, scope: AccessScope.Organization,
roleId,
status: OrgMembershipStatus.Accepted, status: OrgMembershipStatus.Accepted,
isActive: true isActive: true
}, },
tx tx
); );
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
},
tx
);
} else if (orgMembership.status === OrgMembershipStatus.Invited) { } else if (orgMembership.status === OrgMembershipStatus.Invited) {
await orgDAL.updateMembershipById( await orgDAL.updateMembershipById(
orgMembership.id, orgMembership.id,
@@ -459,8 +467,9 @@ export const ldapConfigServiceFactory = ({
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
@@ -469,16 +478,22 @@ export const ldapConfigServiceFactory = ({
await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP); await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP);
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
const membership = await orgDAL.createMembership(
await orgMembershipDAL.create(
{ {
userId: newUser.id, actorUserId: newUser.id,
inviteEmail: email.toLowerCase(), scopeOrgId: orgId,
orgId, scope: AccessScope.Organization,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true,
inviteEmail: email.toLowerCase()
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
}, },
tx tx
); );
@@ -542,10 +557,10 @@ export const ldapConfigServiceFactory = ({
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
membershipGroupDAL,
tx tx
}); });
} }
@@ -566,7 +581,7 @@ export const ldapConfigServiceFactory = ({
userIds: [newUser.id], userIds: [newUser.id],
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
tx tx
}); });
@@ -488,7 +488,7 @@ export const licenseServiceFactory = ({
const getUsageMetrics = async (orgId: string) => { const getUsageMetrics = async (orgId: string) => {
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
orgDAL.countAllOrgMembers(orgId), orgDAL.countAllOrgMembers(orgId),
identityOrgMembershipDAL.countAllOrgIdentities({ orgId }), identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }),
projectDAL.countOfOrgProjects(orgId) projectDAL.countOfOrgProjects(orgId)
]); ]);
@@ -2,7 +2,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -19,12 +19,12 @@ import { OrgServiceActor } from "@app/lib/types";
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
@@ -62,7 +62,8 @@ type TOidcConfigServiceFactoryDep = {
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">; tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail" | "verify">; smtpService: Pick<TSmtpService, "sendMail" | "verify">;
@@ -78,7 +79,6 @@ type TOidcConfigServiceFactoryDep = {
| "delete" | "delete"
| "filterProjectsByUserMembership" | "filterProjectsByUserMembership"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
@@ -90,7 +90,6 @@ export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFacto
export const oidcConfigServiceFactory = ({ export const oidcConfigServiceFactory = ({
orgDAL, orgDAL,
orgMembershipDAL,
userDAL, userDAL,
userAliasDAL, userAliasDAL,
licenseService, licenseService,
@@ -100,7 +99,8 @@ export const oidcConfigServiceFactory = ({
oidcConfigDAL, oidcConfigDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupDAL, groupDAL,
groupProjectDAL, membershipGroupDAL,
membershipRoleDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
@@ -196,26 +196,33 @@ export const oidcConfigServiceFactory = ({
const foundUser = await userDAL.findById(userAlias.userId, tx); const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( const membership = await orgDAL.createMembership(
{ {
userId: userAlias.userId, actorUserId: userAlias.userId,
inviteEmail: email, scopeOrgId: orgId,
orgId, scope: AccessScope.Organization,
role, status: OrgMembershipStatus.Accepted,
roleId,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
tx tx
); );
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
},
tx
);
// Only update the membership to Accepted if the user account is already completed. // Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) { } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
await orgDAL.updateMembershipById( await orgDAL.updateMembershipById(
@@ -288,8 +295,9 @@ export const oidcConfigServiceFactory = ({
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
@@ -299,15 +307,22 @@ export const oidcConfigServiceFactory = ({
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( const membership = await orgDAL.createMembership(
{ {
userId: newUser.id, actorUserId: newUser.id,
inviteEmail: email, scopeOrgId: orgId,
orgId, scope: AccessScope.Organization,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true,
inviteEmail: email.toLowerCase()
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
}, },
tx tx
); );
@@ -341,7 +356,7 @@ export const oidcConfigServiceFactory = ({
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL projectBotDAL
@@ -378,7 +393,7 @@ export const oidcConfigServiceFactory = ({
group, group,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL projectKeyDAL
}); });
} }
@@ -1,389 +0,0 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import {
ProjectPermissionMemberActions,
ProjectPermissionSet,
ProjectPermissionSub
} from "../permission/project-permission";
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
import {
ProjectUserAdditionalPrivilegeTemporaryMode,
TProjectUserAdditionalPrivilegeServiceFactory
} from "./project-user-additional-privilege-types";
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">;
};
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const projectUserAdditionalPrivilegeServiceFactory = ({
projectUserAdditionalPrivilegeDAL,
projectMembershipDAL,
permissionService,
accessApprovalRequestDAL
}: TProjectUserAdditionalPrivilegeServiceFactoryDep): TProjectUserAdditionalPrivilegeServiceFactory => {
const create: TProjectUserAdditionalPrivilegeServiceFactory["create"] = async ({
slug,
actor,
actorId,
permissions: customPermission,
actorOrgId,
actorAuthMethod,
projectMembershipId,
...dto
}) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: projectMembership.userId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
permission,
targetUserPermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged user",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug,
projectId: projectMembership.projectId,
userId: projectMembership.userId
});
if (existingSlug)
throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` });
validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const packedPermission = JSON.stringify(packRules(customPermission));
if (!dto.isTemporary) {
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
userId: projectMembership.userId,
projectId: projectMembership.projectId,
slug,
permissions: packedPermission
});
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectId: projectMembership.projectId,
userId: projectMembership.userId,
slug,
permissions: packedPermission,
isTemporary: true,
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: dto.temporaryRange,
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
});
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const updateById: TProjectUserAdditionalPrivilegeServiceFactory["updateById"] = async ({
privilegeId,
actorOrgId,
actor,
actorId,
actorAuthMethod,
...dto
}) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership)
throw new NotFoundError({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
});
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: projectMembership.userId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// we need to validate that the privilege given is not higher than the assigning users permission
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
permission,
targetUserPermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update more privileged user",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug: dto.slug,
userId: projectMembership.id,
projectId: projectMembership.projectId
});
if (existingSlug && existingSlug.id !== userPrivilege.id)
throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` });
}
validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions));
if (isTemporary) {
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
slug: dto.slug,
permissions: packedPermission,
isTemporary: dto.isTemporary,
temporaryRange: dto.temporaryRange,
temporaryMode: dto.temporaryMode,
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
});
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
}
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
slug: dto.slug,
permissions: packedPermission,
isTemporary: false,
temporaryAccessStartTime: null,
temporaryAccessEndTime: null,
temporaryRange: null,
temporaryMode: null
});
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return {
...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions)
};
};
const deleteById: TProjectUserAdditionalPrivilegeServiceFactory["deleteById"] = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
privilegeId
}) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership)
throw new NotFoundError({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
// TODO(simp): look into this in the new ap service
await accessApprovalRequestDAL.update(
{
privilegeId: userPrivilege.id
},
{
privilegeDeletedAt: new Date(),
status: ApprovalStatus.REJECTED
}
);
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return {
...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions)
};
};
const getPrivilegeDetailsById: TProjectUserAdditionalPrivilegeServiceFactory["getPrivilegeDetailsById"] = async ({
privilegeId,
actorOrgId,
actor,
actorId,
actorAuthMethod
}) => {
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege)
throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` });
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership)
throw new NotFoundError({
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
return {
...userPrivilege,
permissions: unpackPermissions(userPrivilege.permissions)
};
};
const listPrivileges: TProjectUserAdditionalPrivilegeServiceFactory["listPrivileges"] = async ({
projectMembershipId,
actorOrgId,
actor,
actorId,
actorAuthMethod
}) => {
const projectMembership = await projectMembershipDAL.findById(projectMembershipId);
if (!projectMembership)
throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectMembership.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(
{
userId: projectMembership.userId,
projectId: projectMembership.projectId
},
{ sort: [[`${TableName.ProjectUserAdditionalPrivilege}.slug` as "slug", "asc"]] }
);
return userPrivileges;
};
return {
create,
updateById,
deleteById,
getPrivilegeDetailsById,
listPrivileges
};
};
@@ -1,60 +0,0 @@
import { TProjectUserAdditionalPrivilege } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { TProjectPermissionV2Schema } from "../permission/project-permission";
export enum ProjectUserAdditionalPrivilegeTemporaryMode {
Relative = "relative"
}
export type TCreateUserPrivilegeDTO = (
| {
permissions: TProjectPermissionV2Schema[];
projectMembershipId: string;
slug: string;
isTemporary: false;
}
| {
permissions: TProjectPermissionV2Schema[];
projectMembershipId: string;
slug: string;
isTemporary: true;
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
) &
Omit<TProjectPermission, "projectId">;
export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit<TProjectPermission, "projectId"> &
Partial<{
permissions: TProjectPermissionV2Schema[];
slug: 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 };
export type TListUserPrivilegesDTO = Omit<TProjectPermission, "projectId"> & { projectMembershipId: string };
interface TAdditionalPrivilege extends TProjectUserAdditionalPrivilege {
permissions: {
action: string[];
subject?: string | undefined;
conditions?: unknown;
inverted?: boolean | undefined;
}[];
}
export type TProjectUserAdditionalPrivilegeServiceFactory = {
create: (arg: TCreateUserPrivilegeDTO) => Promise<TAdditionalPrivilege>;
updateById: (arg: TUpdateUserPrivilegeDTO) => Promise<TAdditionalPrivilege>;
deleteById: (arg: TDeleteUserPrivilegeDTO) => Promise<TAdditionalPrivilege>;
getPrivilegeDetailsById: (arg: TGetUserPrivilegeDetailsDTO) => Promise<TAdditionalPrivilege>;
listPrivileges: (arg: TListUserPrivilegesDTO) => Promise<TProjectUserAdditionalPrivilege[]>;
};
@@ -10,7 +10,8 @@ import {
TGroups, TGroups,
TSamlConfigs, TSamlConfigs,
TSamlConfigsUpdate, TSamlConfigsUpdate,
TUsers TUsers,
AccessScope
} from "@app/db/schemas"; } from "@app/db/schemas";
import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns"; import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -23,12 +24,9 @@ import { TGroupProjectDALFactory } from "@app/services/group-project/group-proje
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { LoginMethod } from "@app/services/super-admin/super-admin-types";
@@ -68,16 +66,7 @@ type TSamlConfigServiceFactoryDep = {
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>; >;
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">; identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">; membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "find" | "transaction">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">; tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
@@ -88,7 +77,6 @@ type TSamlConfigServiceFactoryDep = {
export const samlConfigServiceFactory = ({ export const samlConfigServiceFactory = ({
samlConfigDAL, samlConfigDAL,
orgDAL, orgDAL,
orgMembershipDAL,
userDAL, userDAL,
userAliasDAL, userAliasDAL,
groupDAL, groupDAL,
@@ -102,7 +90,8 @@ export const samlConfigServiceFactory = ({
tokenService, tokenService,
smtpService, smtpService,
identityMetadataDAL, identityMetadataDAL,
kmsService kmsService,
membershipRoleDAL
}: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => { }: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => {
const parseSamlGroups = (groupsValue: string): string[] => { const parseSamlGroups = (groupsValue: string): string[] => {
let samlGroups: string[] = []; let samlGroups: string[] = [];
@@ -506,26 +495,35 @@ export const samlConfigServiceFactory = ({
const foundUser = await userDAL.findById(userAlias.userId, tx); const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( const membership = await orgDAL.createMembership(
{ {
userId: userAlias.userId, actorUserId: userAlias.userId,
inviteEmail: email, inviteEmail: email,
orgId, scopeOrgId: orgId,
role, scope: AccessScope.Organization,
roleId, status: OrgMembershipStatus.Accepted,
status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited,
isActive: true isActive: true
}, },
tx tx
); );
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
},
tx
);
// Only update the membership to Accepted if the user account is already completed. // Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) { } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
await orgDAL.updateMembershipById( await orgDAL.updateMembershipById(
@@ -606,8 +604,9 @@ export const samlConfigServiceFactory = ({
const [orgMembership] = await orgDAL.findMembership( const [orgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
@@ -617,15 +616,22 @@ export const samlConfigServiceFactory = ({
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create( const membership = await orgDAL.createMembership(
{ {
userId: newUser.id, actorUserId: newUser.id,
inviteEmail: email, scopeOrgId: orgId,
orgId, scope: AccessScope.Organization,
role,
roleId,
status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true,
inviteEmail: email.toLowerCase()
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
}, },
tx tx
); );
+96 -68
View File
@@ -2,7 +2,15 @@ import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { scimPatch } from "scim-patch"; import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; import {
AccessScope,
OrgMembershipRole,
OrgMembershipStatus,
TableName,
TGroups,
TMemberships,
TUsers
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -13,12 +21,13 @@ import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } f
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal";
import { TMembershipUserDALFactory } from "@app/services/membership-user/membership-user-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembershipFn } from "@app/services/org/org-fns"; import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { OrgAuthMethod } from "@app/services/org/org-types"; import { OrgAuthMethod } from "@app/services/org/org-types";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
@@ -33,7 +42,6 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import { TScimGroup, TScimServiceFactory } from "./scim-types"; import { TScimGroup, TScimServiceFactory } from "./scim-types";
@@ -55,12 +63,9 @@ type TScimServiceFactoryDep = {
| "updateMembershipById" | "updateMembershipById"
| "findOrgById" | "findOrgById"
>; >;
orgMembershipDAL: Pick< membershipUserDAL: TMembershipUserDALFactory;
TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
>;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
| "create" | "create"
@@ -72,7 +77,8 @@ type TScimServiceFactoryDep = {
| "updateById" | "updateById"
| "update" | "update"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create" | "update">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
| "find" | "find"
@@ -88,7 +94,6 @@ type TScimServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
}; };
@@ -98,18 +103,18 @@ export const scimServiceFactory = ({
userDAL, userDAL,
userAliasDAL, userAliasDAL,
orgDAL, orgDAL,
orgMembershipDAL,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
groupDAL, groupDAL,
groupProjectDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
projectKeyDAL, projectKeyDAL,
projectBotDAL, projectBotDAL,
permissionService, permissionService,
projectUserAdditionalPrivilegeDAL,
smtpService, smtpService,
externalGroupOrgRoleMappingDAL externalGroupOrgRoleMappingDAL,
membershipGroupDAL,
membershipUserDAL,
membershipRoleDAL
}: TScimServiceFactoryDep): TScimServiceFactory => { }: TScimServiceFactoryDep): TScimServiceFactory => {
const createScimToken: TScimServiceFactory["createScimToken"] = async ({ const createScimToken: TScimServiceFactory["createScimToken"] = async ({
actor, actor,
@@ -244,8 +249,9 @@ export const scimServiceFactory = ({
const getScimUser: TScimServiceFactory["getScimUser"] = async ({ orgMembershipId, orgId }) => { const getScimUser: TScimServiceFactory["getScimUser"] = async ({ orgMembershipId, orgId }) => {
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.Membership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -322,13 +328,14 @@ export const scimServiceFactory = ({
const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => { const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => {
let user: TUsers | undefined; let user: TUsers | undefined;
let orgMembership: TOrgMemberships; let orgMembership: TMemberships;
if (userAlias) { if (userAlias) {
user = await userDAL.findById(userAlias.userId, tx); user = await userDAL.findById(userAlias.userId, tx);
orgMembership = await orgMembershipDAL.findOne( orgMembership = await membershipUserDAL.findOne(
{ {
userId: user.id, actorUserId: user.id,
orgId scope: AccessScope.Organization,
scopeOrgId: orgId
}, },
tx tx
); );
@@ -336,20 +343,24 @@ export const scimServiceFactory = ({
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create( orgMembership = await membershipUserDAL.create(
{ {
userId: userAlias.userId, actorUserId: userAlias.userId,
inviteEmail: email.toLowerCase(), inviteEmail: email.toLowerCase(),
orgId, scopeOrgId: orgId,
role, scope: AccessScope.Organization,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
tx tx
); );
await membershipRoleDAL.create({
membershipId: orgMembership.id,
role,
customRoleId: roleId
});
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgMembershipDAL.updateById( orgMembership = await membershipUserDAL.updateById(
orgMembership.id, orgMembership.id,
{ {
status: OrgMembershipStatus.Accepted status: OrgMembershipStatus.Accepted
@@ -401,8 +412,9 @@ export const scimServiceFactory = ({
const [foundOrgMembership] = await orgDAL.findMembership( const [foundOrgMembership] = await orgDAL.findMembership(
{ {
[`${TableName.OrgMembership}.userId` as "userId"]: user.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}, },
{ tx } { tx }
); );
@@ -412,18 +424,25 @@ export const scimServiceFactory = ({
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
orgMembership = await orgMembershipDAL.create( orgMembership = await membershipUserDAL.create(
{ {
userId: user.id, actorUserId: user.id,
inviteEmail: email.toLowerCase(), inviteEmail: email.toLowerCase(),
orgId, scopeOrgId: orgId,
role, scope: AccessScope.Organization,
roleId,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true isActive: true
}, },
tx tx
); );
await membershipRoleDAL.create(
{
membershipId: orgMembership.id,
role,
customRoleId: roleId
},
tx
);
// Only update the membership to Accepted if the user account is already completed. // Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) {
orgMembership = await orgDAL.updateMembershipById( orgMembership = await orgDAL.updateMembershipById(
@@ -475,8 +494,9 @@ export const scimServiceFactory = ({
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.Membership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -485,7 +505,7 @@ export const scimServiceFactory = ({
}); });
}); });
if (!membership) if (!membership || !membership.actorUserId)
throw new ScimRequestError({ throw new ScimRequestError({
detail: "User not found", detail: "User not found",
status: 404 status: 404
@@ -514,7 +534,7 @@ export const scimServiceFactory = ({
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
await userDAL.transaction(async (tx) => { await userDAL.transaction(async (tx) => {
await orgMembershipDAL.updateById( await membershipUserDAL.updateById(
membership.id, membership.id,
{ {
isActive: scimUser.active isActive: scimUser.active
@@ -523,7 +543,7 @@ export const scimServiceFactory = ({
); );
const hasEmailChanged = scimUser.emails[0].value !== membership.email; const hasEmailChanged = scimUser.emails[0].value !== membership.email;
await userDAL.updateById( await userDAL.updateById(
membership.userId, membership.actorUserId as string,
{ {
firstName: scimUser.name.givenName, firstName: scimUser.name.givenName,
email: scimUser.emails[0].value.toLowerCase(), email: scimUser.emails[0].value.toLowerCase(),
@@ -556,8 +576,9 @@ export const scimServiceFactory = ({
const [membership] = await orgDAL const [membership] = await orgDAL
.findMembership({ .findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.Membership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}) })
.catch(() => { .catch(() => {
throw new ScimRequestError({ throw new ScimRequestError({
@@ -566,7 +587,7 @@ export const scimServiceFactory = ({
}); });
}); });
if (!membership) if (!membership || !membership.actorUserId)
throw new ScimRequestError({ throw new ScimRequestError({
detail: "User not found", detail: "User not found",
status: 404 status: 404
@@ -587,7 +608,7 @@ export const scimServiceFactory = ({
{ {
orgId, orgId,
aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML, aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML,
userId: membership.userId userId: membership.actorUserId as string
}, },
{ {
externalId externalId
@@ -595,7 +616,7 @@ export const scimServiceFactory = ({
tx tx
); );
await orgMembershipDAL.updateById( await membershipUserDAL.updateById(
membership.id, membership.id,
{ {
isActive: active isActive: active
@@ -603,7 +624,7 @@ export const scimServiceFactory = ({
tx tx
); );
await userDAL.updateById( await userDAL.updateById(
membership.userId, membership.actorUserId!,
{ {
firstName, firstName,
email: email?.toLowerCase(), email: email?.toLowerCase(),
@@ -628,8 +649,9 @@ export const scimServiceFactory = ({
const deleteScimUser: TScimServiceFactory["deleteScimUser"] = async ({ orgMembershipId, orgId }) => { const deleteScimUser: TScimServiceFactory["deleteScimUser"] = async ({ orgMembershipId, orgId }) => {
const [membership] = await orgDAL.findMembership({ const [membership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, [`${TableName.Membership}.id` as "id"]: orgMembershipId,
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}); });
if (!membership) if (!membership)
@@ -647,13 +669,13 @@ export const scimServiceFactory = ({
await deleteOrgMembershipFn({ await deleteOrgMembershipFn({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.scopeOrgId,
orgDAL, orgDAL,
projectMembershipDAL, projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL, projectKeyDAL,
userAliasDAL, userAliasDAL,
licenseService licenseService,
membershipUserDAL
}); });
return {}; // intentionally return empty object upon success return {}; // intentionally return empty object upon success
@@ -750,8 +772,9 @@ export const scimServiceFactory = ({
if (!externalGroupMapping) return; if (!externalGroupMapping) return;
// only get org memberships that are new (invites) // only get org memberships that are new (invites)
const newOrgMemberships = await orgMembershipDAL.find({ const newOrgMemberships = await membershipUserDAL.find({
status: "invited", status: "invited",
scope: AccessScope.Organization,
$in: { $in: {
id: members.map((member) => member.value) id: members.map((member) => member.value)
} }
@@ -760,7 +783,7 @@ export const scimServiceFactory = ({
if (!newOrgMemberships.length) return; if (!newOrgMemberships.length) return;
// set new membership roles to group mapping value // set new membership roles to group mapping value
await orgMembershipDAL.update( await membershipRoleDAL.update(
{ {
$in: { $in: {
id: newOrgMemberships.map((membership) => membership.id) id: newOrgMemberships.map((membership) => membership.id)
@@ -768,7 +791,7 @@ export const scimServiceFactory = ({
}, },
{ {
role: externalGroupMapping.role, role: externalGroupMapping.role,
roleId: externalGroupMapping.roleId customRoleId: externalGroupMapping.roleId
} }
); );
}; };
@@ -822,7 +845,8 @@ export const scimServiceFactory = ({
); );
if (members && members.length) { if (members && members.length) {
const orgMemberships = await orgMembershipDAL.find({ const orgMemberships = await membershipUserDAL.find({
scope: AccessScope.Organization,
$in: { $in: {
id: members.map((member) => member.value) id: members.map((member) => member.value)
} }
@@ -830,14 +854,14 @@ export const scimServiceFactory = ({
const newMembers = await addUsersToGroupByUserIds({ const newMembers = await addUsersToGroupByUserIds({
group, group,
userIds: orgMemberships.map((membership) => membership.userId as string), userIds: orgMemberships.map((membership) => membership.actorUserId as string),
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
membershipGroupDAL,
tx tx
}); });
@@ -850,9 +874,10 @@ export const scimServiceFactory = ({
}); });
const orgMemberships = await orgDAL.findMembership({ const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization,
$in: { $in: {
[`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id) [`${TableName.Membership}.actorUserId` as "actorUserId"]: newGroup.newMembers.map((member) => member.id)
} }
}); });
@@ -895,9 +920,10 @@ export const scimServiceFactory = ({
.then((g) => g.members); .then((g) => g.members);
const orgMemberships = await orgDAL.findMembership({ const orgMemberships = await orgDAL.findMembership({
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization,
$in: { $in: {
[`${TableName.OrgMembership}.userId` as "userId"]: users [`${TableName.Membership}.actorUserId` as "actorUserId"]: users
.filter((user) => user.isPartOfGroup) .filter((user) => user.isPartOfGroup)
.map((user) => user.id) .map((user) => user.id)
} }
@@ -933,10 +959,10 @@ export const scimServiceFactory = ({
} }
const updatedGroup = await groupDAL.transaction(async (tx) => { const updatedGroup = await groupDAL.transaction(async (tx) => {
if (group.name !== displayName) { if (group?.name !== displayName) {
await externalGroupOrgRoleMappingDAL.update( await externalGroupOrgRoleMappingDAL.update(
{ {
groupName: group.name, groupName: group?.name,
orgId orgId
}, },
{ {
@@ -958,14 +984,16 @@ export const scimServiceFactory = ({
} }
const orgMemberships = members.length const orgMemberships = members.length
? await orgMembershipDAL.find({ ? await membershipUserDAL.find({
[`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization,
$in: { $in: {
id: members.map((member) => member.value) id: members.map((member) => member.value)
} }
}) })
: []; : [];
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId)); const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.actorUserId as string));
const userGroupMembers = await userGroupMembershipDAL.find({ const userGroupMembers = await userGroupMembershipDAL.find({
groupId: group.id groupId: group.id
}); });
@@ -978,20 +1006,20 @@ export const scimServiceFactory = ({
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds); const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds); const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string)); const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.actorUserId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId)); const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) { if (toAddUserIds.length) {
await addUsersToGroupByUserIds({ await addUsersToGroupByUserIds({
group, group,
userIds: toAddUserIds.map((member) => member.userId as string), userIds: toAddUserIds.map((member) => member.actorUserId as string),
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL, orgDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
membershipGroupDAL,
tx tx
}); });
} }
@@ -1002,7 +1030,7 @@ export const scimServiceFactory = ({
userIds: toRemoveUserIds, userIds: toRemoveUserIds,
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL, membershipGroupDAL,
projectKeyDAL, projectKeyDAL,
tx tx
}); });
@@ -1,10 +1,12 @@
import { z } from "zod"; import { z } from "zod";
import { import {
AccessScope,
GroupProjectMembershipsSchema, GroupProjectMembershipsSchema,
GroupsSchema, GroupsSchema,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUserMembershipRolesSchema, ProjectUserMembershipRolesSchema,
TemporaryPermissionMode,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
@@ -13,7 +15,6 @@ import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { export const registerGroupProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -54,7 +55,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
z.object({ z.object({
role: z.string(), role: z.string(),
isTemporary: z.literal(true), isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), temporaryMode: z.nativeEnum(TemporaryPermissionMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime() temporaryAccessStartTime: z.string().datetime()
}) })
@@ -73,17 +74,27 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMembership = await server.services.groupProject.addGroupToProject({ // TODO(simp): name to id convertor
actor: req.permission.type, const { membership: groupMembership } = await server.services.membershipGroup.createMembership({
actorId: req.permission.id, permission: req.permission,
actorAuthMethod: req.permission.authMethod, data: {
actorOrgId: req.permission.orgId, groupId: req.params.groupIdOrName,
roles: req.body.roles || [{ role: req.body.role }], roles: req.body.roles || [{ role: req.body.role, isTemporary: false }]
projectId: req.params.projectId, },
groupIdOrName: req.params.groupIdOrName scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMembership }; return {
groupMembership: {
...groupMembership,
projectId: req.params.projectId,
groupId: groupMembership.actorGroupId as string
}
};
} }
}); });
@@ -115,7 +126,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
z.object({ z.object({
role: z.string(), role: z.string(),
isTemporary: z.literal(true), isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), temporaryMode: z.nativeEnum(TemporaryPermissionMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime() temporaryAccessStartTime: z.string().datetime()
}) })
@@ -131,17 +142,22 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
} }
}, },
handler: async (req) => { handler: async (req) => {
const roles = await server.services.groupProject.updateGroupInProject({ const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, selector: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupId
actorOrgId: req.permission.orgId, },
projectId: req.params.projectId, data: {
groupId: req.params.groupId, roles: req.body.roles
roles: req.body.roles },
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { roles }; return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) };
} }
}); });
@@ -172,16 +188,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMembership = await server.services.groupProject.removeGroupFromProject({ const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, selector: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupId
actorOrgId: req.permission.orgId, },
groupId: req.params.groupId, scopeData: {
projectId: req.params.projectId scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMembership }; return {
groupMembership: {
...groupMembership,
projectId: req.params.projectId,
groupId: groupMembership.actorGroupId as string
}
};
} }
}); });
@@ -233,15 +258,17 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMemberships = await server.services.groupProject.listGroupsInProject({ const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, data: {},
actorAuthMethod: req.permission.authMethod, scopeData: {
actorOrgId: req.permission.orgId, scope: AccessScope.Project,
projectId: req.params.projectId orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMemberships }; return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) };
} }
}); });
@@ -292,15 +319,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMembership = await server.services.groupProject.getGroupInProject({ const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, selector: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupId
actorOrgId: req.permission.orgId, },
...req.params scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMembership }; return {
groupMembership: {
...groupMembership,
projectId: req.params.projectId,
groupId: groupMembership.actorGroupId as string
}
};
} }
}); });
@@ -1,10 +1,12 @@
import { z } from "zod"; import { z } from "zod";
import { import {
AccessScope,
GroupProjectMembershipsSchema, GroupProjectMembershipsSchema,
GroupsSchema, GroupsSchema,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUserMembershipRolesSchema, ProjectUserMembershipRolesSchema,
TemporaryPermissionMode,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
@@ -13,7 +15,6 @@ import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => { export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -54,7 +55,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
z.object({ z.object({
role: z.string(), role: z.string(),
isTemporary: z.literal(true), isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), temporaryMode: z.nativeEnum(TemporaryPermissionMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime() temporaryAccessStartTime: z.string().datetime()
}) })
@@ -73,17 +74,26 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMembership = await server.services.groupProject.addGroupToProject({ const { membership: groupMembership } = await server.services.membershipGroup.createMembership({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, data: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupIdOrName,
actorOrgId: req.permission.orgId, roles: req.body.roles || [{ role: req.body.role, isTemporary: false }]
roles: req.body.roles || [{ role: req.body.role }], },
projectId: req.params.projectId, scopeData: {
groupIdOrName: req.params.groupIdOrName scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMembership }; return {
groupMembership: {
...groupMembership,
projectId: req.params.projectId,
groupId: groupMembership.actorGroupId as string
}
};
} }
}); });
@@ -115,7 +125,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
z.object({ z.object({
role: z.string(), role: z.string(),
isTemporary: z.literal(true), isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), temporaryMode: z.nativeEnum(TemporaryPermissionMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime() temporaryAccessStartTime: z.string().datetime()
}) })
@@ -131,17 +141,22 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
} }
}, },
handler: async (req) => { handler: async (req) => {
const roles = await server.services.groupProject.updateGroupInProject({ const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, selector: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupId
actorOrgId: req.permission.orgId, },
projectId: req.params.projectId, data: {
groupId: req.params.groupId, roles: req.body.roles
roles: req.body.roles },
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { roles }; return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) };
} }
}); });
@@ -172,16 +187,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMembership = await server.services.groupProject.removeGroupFromProject({ const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, selector: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupId
actorOrgId: req.permission.orgId, },
groupId: req.params.groupId, scopeData: {
projectId: req.params.projectId scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMembership }; return {
groupMembership: {
...groupMembership,
projectId: req.params.projectId,
groupId: groupMembership.actorGroupId as string
}
};
} }
}); });
@@ -233,15 +257,17 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMemberships = await server.services.groupProject.listGroupsInProject({ const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, data: {},
actorAuthMethod: req.permission.authMethod, scopeData: {
actorOrgId: req.permission.orgId, scope: AccessScope.Project,
projectId: req.params.projectId orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMemberships }; return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) };
} }
}); });
@@ -292,15 +318,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro
} }
}, },
handler: async (req) => { handler: async (req) => {
const groupMembership = await server.services.groupProject.getGroupInProject({ const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({
actor: req.permission.type, permission: req.permission,
actorId: req.permission.id, selector: {
actorAuthMethod: req.permission.authMethod, groupId: req.params.groupId
actorOrgId: req.permission.orgId, },
...req.params scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
}
}); });
return { groupMembership }; return {
groupMembership: {
...groupMembership,
projectId: req.params.projectId,
groupId: groupMembership.actorGroupId as string
}
};
} }
}); });
@@ -1,12 +1,12 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal"; import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
@@ -14,7 +14,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
type TAuthTokenServiceFactoryDep = { type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory; tokenDAL: TTokenDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "transaction">; userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOne">; membershipUserDAL: Pick<TMembershipUserDALFactory, "findOne">;
}; };
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>; export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
@@ -80,7 +80,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
} }
}; };
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => {
const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type); const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig(); const appCfg = getConfig();
@@ -208,9 +208,10 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
if (token.organizationId) { if (token.organizationId) {
const orgMembership = await orgMembershipDAL.findOne({ const orgMembership = await membershipUserDAL.findOne({
userId: user.id, actorUserId: user.id,
orgId: token.organizationId scopeOrgId: token.organizationId,
scope: AccessScope.Organization
}); });
if (!orgMembership) { if (!orgMembership) {
+45 -21
View File
@@ -1,6 +1,13 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; import {
AccessScope,
OrgMembershipRole,
OrgMembershipStatus,
TableName,
TUsers,
UserDeviceSchema
} from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -14,11 +21,12 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
import { TNotificationServiceFactory } from "../notification/notification-service"; import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types"; import { NotificationType } from "../notification/notification-types";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { getDefaultOrgMembershipRole } from "../org/org-role-fns"; import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { LoginMethod } from "../super-admin/super-admin-types"; import { LoginMethod } from "../super-admin/super-admin-types";
import { TTotpServiceFactory } from "../totp/totp-service"; import { TTotpServiceFactory } from "../totp/totp-service";
@@ -48,7 +56,8 @@ type TAuthLoginServiceFactoryDep = {
smtpService: TSmtpService; smtpService: TSmtpService;
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">; totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
orgMembershipDAL: TOrgMembershipDALFactory; membershipUserDAL: TMembershipUserDALFactory;
membershipRoleDAL: TMembershipRoleDALFactory;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">; notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
}; };
@@ -58,10 +67,11 @@ export const authLoginServiceFactory = ({
tokenService, tokenService,
smtpService, smtpService,
orgDAL, orgDAL,
orgMembershipDAL,
totpService, totpService,
auditLogService, auditLogService,
notificationService notificationService,
membershipUserDAL,
membershipRoleDAL
}: TAuthLoginServiceFactoryDep) => { }: TAuthLoginServiceFactoryDep) => {
/* /*
* Private * Private
@@ -163,8 +173,8 @@ export const authLoginServiceFactory = ({
if (organizationId) { if (organizationId) {
const org = await orgDAL.findById(organizationId); const org = await orgDAL.findById(organizationId);
if (org) { if (org) {
await orgMembershipDAL.update( await membershipUserDAL.update(
{ userId: user.id, orgId: org.id }, { actorUserId: user.id, scopeOrgId: org.id, scope: AccessScope.Organization },
{ lastLoginAuthMethod: authMethod, lastLoginTime: new Date() } { lastLoginAuthMethod: authMethod, lastLoginTime: new Date() }
); );
if (org.userTokenExpiration) { if (org.userTokenExpiration) {
@@ -858,21 +868,34 @@ export const authLoginServiceFactory = ({
} }
orgId = defaultOrg.id; orgId = defaultOrg.id;
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}); });
if (!orgMembership) { if (!orgMembership) {
const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole); const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole);
await orgMembershipDAL.create({ await membershipUserDAL.transaction(async (tx) => {
userId: user.id, const membership = await membershipUserDAL.create(
inviteEmail: email, {
orgId, actorUserId: user?.id,
role, inviteEmail: email,
roleId, scopeOrgId: orgId,
status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization,
isActive: true status: OrgMembershipStatus.Accepted,
isActive: true
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role,
customRoleId: roleId
},
tx
);
}); });
} }
} }
@@ -895,10 +918,11 @@ export const authLoginServiceFactory = ({
if (org) { if (org) {
// checks for the membership and only sets the orgId / orgName if the user is a member of the specified org // checks for the membership and only sets the orgId / orgName if the user is a member of the specified org
const orgMembership = await orgDAL.findMembership({ const orgMembership = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id,
[`${TableName.OrgMembership}.orgId` as "orgId"]: org.id, [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: org.id,
[`${TableName.OrgMembership}.isActive` as "isActive"]: true, [`${TableName.Membership}.isActive` as "isActive"]: true,
[`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted [`${TableName.Membership}.status` as "status"]: OrgMembershipStatus.Accepted,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}); });
if (orgMembership) { if (orgMembership) {
@@ -1,4 +1,4 @@
import { OrgMembershipStatus, TableName } from "@app/db/schemas"; import { AccessScope, OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -8,17 +8,15 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { getMinExpiresIn } from "@app/lib/fn"; import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TMembershipGroupDALFactory } from "../membership-group/membership-group-dal";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { getServerCfg } from "../super-admin/super-admin-service"; import { getServerCfg } from "../super-admin/super-admin-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
@@ -42,14 +40,12 @@ type TAuthSignupDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">; orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">; membershipGroupDAL: TMembershipGroupDALFactory;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>; export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
@@ -60,11 +56,11 @@ export const authSignupServiceFactory = ({
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
groupProjectDAL,
tokenService, tokenService,
smtpService, smtpService,
orgService, orgService,
orgDAL, orgDAL,
membershipGroupDAL,
licenseService licenseService
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
@@ -200,9 +196,10 @@ export const authSignupServiceFactory = ({
organizationId organizationId
) { ) {
const [pendingOrgMembership] = await orgDAL.findMembership({ const [pendingOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id, [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
[`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: organizationId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization
}); });
if (pendingOrgMembership) { if (pendingOrgMembership) {
@@ -241,18 +238,18 @@ export const authSignupServiceFactory = ({
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization },
{ userId: user.id, status: OrgMembershipStatus.Accepted } { actorUserId: user.id, status: OrgMembershipStatus.Accepted }
); );
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))];
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
await convertPendingGroupAdditionsToGroupMemberships({ await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id], userIds: [user.id],
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
membershipGroupDAL,
projectDAL, projectDAL,
projectBotDAL projectBotDAL
}); });
@@ -351,21 +348,21 @@ export const authSignupServiceFactory = ({
); );
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { actorUserId: us.id, status: OrgMembershipStatus.Accepted },
tx tx
); );
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))];
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx))); await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx)));
await convertPendingGroupAdditionsToGroupMemberships({ await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id], userIds: [user.id],
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL, projectKeyDAL,
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
membershipGroupDAL,
tx tx
}); });
@@ -1,19 +1,19 @@
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TRoles } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { isCustomOrgRole } from "@app/services/org/org-role-fns"; import { isCustomOrgRole } from "@app/services/org/org-role-fns";
import { TRoleDALFactory } from "../role/role-dal";
import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types"; import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types";
export const constructGroupOrgMembershipRoleMappings = async ({ export const constructGroupOrgMembershipRoleMappings = async ({
mappingsDTO, mappingsDTO,
orgId, orgId,
orgRoleDAL, roleDAL,
licenseService licenseService
}: { }: {
mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[]; mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[];
orgRoleDAL: TOrgRoleDALFactory; roleDAL: TRoleDALFactory;
licenseService: TLicenseServiceFactory; licenseService: TLicenseServiceFactory;
orgId: string; orgId: string;
}) => { }) => {
@@ -30,9 +30,9 @@ export const constructGroupOrgMembershipRoleMappings = async ({
.filter((mapping) => isCustomOrgRole(mapping.roleSlug)) .filter((mapping) => isCustomOrgRole(mapping.roleSlug))
.map((mapping) => mapping.roleSlug); .map((mapping) => mapping.roleSlug);
let customRolesMap: Map<string, TOrgRoles> = new Map(); let customRolesMap: Map<string, TRoles> = new Map();
if (customRoleSlugs.length > 0) { if (customRoleSlugs.length > 0) {
const customRoles = await orgRoleDAL.find({ const customRoles = await roleDAL.find({
orgId, orgId,
$in: { $in: {
slug: customRoleSlugs slug: customRoleSlugs
@@ -6,15 +6,15 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TRoleDALFactory } from "../role/role-dal";
import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal"; import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal";
type TExternalGroupOrgRoleMappingServiceFactoryDep = { type TExternalGroupOrgRoleMappingServiceFactoryDep = {
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
licenseService: TLicenseServiceFactory; licenseService: TLicenseServiceFactory;
orgRoleDAL: TOrgRoleDALFactory; roleDAL: TRoleDALFactory;
}; };
export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType<typeof externalGroupOrgRoleMappingServiceFactory>; export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType<typeof externalGroupOrgRoleMappingServiceFactory>;
@@ -23,7 +23,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
externalGroupOrgRoleMappingDAL, externalGroupOrgRoleMappingDAL,
licenseService, licenseService,
permissionService, permissionService,
orgRoleDAL roleDAL
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => { }: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
@@ -61,7 +61,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
const mappings = await constructGroupOrgMembershipRoleMappings({ const mappings = await constructGroupOrgMembershipRoleMappings({
mappingsDTO: dto.mappings, mappingsDTO: dto.mappings,
orgRoleDAL, roleDAL,
licenseService, licenseService,
orgId: actor.orgId orgId: actor.orgId
}); });
@@ -1,504 +1,27 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { crypto } from "@app/lib/crypto/cryptography"; import { NotFoundError } from "@app/lib/errors";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal"; import { TGroupDALFactory } from "../../ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TGroupProjectDALFactory } from "./group-project-dal";
import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal";
import {
TCreateProjectGroupDTO,
TDeleteProjectGroupDTO,
TGetGroupInProjectDTO,
TListProjectGroupDTO,
TUpdateProjectGroupDTO
} from "./group-project-types";
type TGroupProjectServiceFactoryDep = { type TGroupProjectServiceFactoryDep = {
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">;
groupProjectMembershipRoleDAL: Pick<
TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete"
>;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembersNotInProject" | "find">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser" | "findById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: TProjectBotDALFactory;
groupDAL: Pick<TGroupDALFactory, "findOne" | "findAllGroupPossibleMembers">; groupDAL: Pick<TGroupDALFactory, "findOne" | "findAllGroupPossibleMembers">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRoles" | "invalidateProjectPermissionCache">; projectDAL: Pick<TProjectDALFactory, "findOne" | "findProjectGhostUser" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRoles">;
}; };
export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>; export type TGroupProjectServiceFactory = ReturnType<typeof groupProjectServiceFactory>;
export const groupProjectServiceFactory = ({ export const groupProjectServiceFactory = ({
groupDAL, groupDAL,
groupProjectDAL,
groupProjectMembershipRoleDAL,
userGroupMembershipDAL,
projectDAL, projectDAL,
projectKeyDAL,
projectBotDAL,
projectRoleDAL,
permissionService permissionService
}: TGroupProjectServiceFactoryDep) => { }: TGroupProjectServiceFactoryDep) => {
const addGroupToProject = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
roles,
projectId,
groupIdOrName
}: TCreateProjectGroupDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups);
let group: TGroups | null = null;
if (isUuidV4(groupIdOrName)) {
group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName });
}
if (!group) {
group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName });
}
if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` });
const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (existingGroup)
throw new BadRequestError({
message: `Group with ID ${group.id} already exists in project with id ${project.id}`
});
for await (const { role: requestedRoleChange } of roles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
requestedRoleChange,
project.id
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to assign group to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId: project.id,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) {
const customRoleSlugs = customRoles.map((customRole) => customRole.slug);
const missingInputRoles = customInputRoles
.filter((inputRole) => !customRoleSlugs.includes(inputRole.role))
.map((role) => role.role);
throw new NotFoundError({
message: `Custom role/s not found: ${missingInputRoles.join(", ")}`
});
}
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const projectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupProjectMembership = await groupProjectDAL.create(
{
groupId: group!.id,
projectId: project.id
},
tx
);
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: groupProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: groupProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
isTemporary: true,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
// share project key with users in group that have not
// individually been added to the project and that are not part of
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
if (!ghostUser) {
throw new NotFoundError({
message: `Failed to find project owner of project with name ${project.name}`
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id, tx);
if (!ghostUserLatestKey) {
throw new NotFoundError({
message: `Failed to find project owner's latest key in project with name ${project.name}`
});
}
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's latest key in project with name ${project.name}`
});
}
const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
if (!bot) {
throw new NotFoundError({
message: `Failed to find project bot in project with name ${project.name}`
});
}
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: ghostUserLatestKey.encryptedKey,
nonce: ghostUserLatestKey.nonce,
publicKey: ghostUserLatestKey.sender.publicKey,
privateKey: botPrivateKey
});
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
if (!publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with name ${project.name}`
});
}
const { ciphertext: encryptedKey, nonce } = crypto
.encryption()
.asymmetric()
.encrypt(plaintextProjectKey, publicKey, botPrivateKey);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: id,
projectId: project.id
};
});
await projectKeyDAL.insertMany(projectKeyData, tx);
}
return groupProjectMembership;
});
await permissionService.invalidateProjectPermissionCache(projectId);
return projectGroup;
};
const updateGroupInProject = async ({
projectId,
groupId,
roles,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TUpdateProjectGroupDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
const { permission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups);
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (!projectGroup) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
for await (const { role: requestedRoleChange } of roles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
requestedRoleChange,
project.id
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to assign group to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionGroupActions.GrantPrivileges,
ProjectPermissionSub.Groups
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId: project.id,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) {
const customRoleSlugs = customRoles.map((customRole) => customRole.slug);
const missingInputRoles = customInputRoles
.filter((inputRole) => !customRoleSlugs.includes(inputRole.role))
.map((role) => role.role);
throw new NotFoundError({
message: `Custom role/s not found: ${missingInputRoles.join(", ")}`
});
}
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: projectGroup.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: projectGroup.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
isTemporary: true,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => {
await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx);
return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
await permissionService.invalidateProjectPermissionCache(projectId);
return updatedRoles;
};
const removeGroupFromProject = async ({
projectId,
groupId,
actorId,
actor,
actorOrgId,
actorAuthMethod
}: TDeleteProjectGroupDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id });
if (!groupProjectMembership) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups);
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupMembersNotInProject = await userGroupMembershipDAL.findGroupMembersNotInProject(
group.id,
project.id,
tx
);
if (groupMembersNotInProject.length) {
await projectKeyDAL.delete(
{
projectId: project.id,
$in: {
receiverId: groupMembersNotInProject.map(({ user: { id } }) => id)
}
},
tx
);
}
const [projectGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id }, tx);
return projectGroup;
});
await permissionService.invalidateProjectPermissionCache(projectId);
return deletedProjectGroup;
};
const listGroupsInProject = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectGroupDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
return groupMemberships;
};
const getGroupInProject = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
groupId,
projectId
}: TGetGroupInProjectDTO) => {
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, {
groupId
});
if (!groupMembership) {
throw new NotFoundError({
message: `Group membership with ID ${groupId} not found in project with ID ${projectId}`
});
}
return groupMembership;
};
const listProjectGroupUsers = async ({ const listProjectGroupUsers = async ({
id, id,
projectId, projectId,
@@ -542,11 +65,6 @@ export const groupProjectServiceFactory = ({
}; };
return { return {
addGroupToProject,
updateGroupInProject,
removeGroupFromProject,
listGroupsInProject,
getGroupInProject,
listProjectGroupUsers listProjectGroupUsers
}; };
}; };
@@ -217,8 +217,8 @@ export const membershipGroupServiceFactory = ({
}, },
tx tx
); );
await membershipRoleDAL.insertMany(roleDocs, tx); const roles = await membershipRoleDAL.insertMany(roleDocs, tx);
return doc; return { ...doc, roles };
}); });
return { membership: membershipDoc }; return { membership: membershipDoc };
@@ -276,7 +276,7 @@ export const membershipGroupServiceFactory = ({
: undefined : undefined
} }
}); });
return memberships; return { memberships: memberships.data, totalCount: memberships.totalCount };
}; };
const getMembershipByGroupId = async (dto: TGetMembershipGroupByGroupIdDTO) => { const getMembershipByGroupId = async (dto: TGetMembershipGroupByGroupIdDTO) => {
@@ -290,7 +290,7 @@ export const membershipGroupServiceFactory = ({
}); });
if (!membership) throw new NotFoundError({ message: `Group membership not found` }); if (!membership) throw new NotFoundError({ message: `Group membership not found` });
return membership; return { membership };
}; };
return { return {
@@ -54,14 +54,11 @@ export type TUpdateMembershipGroupDTO = {
export type TListMembershipGroupDTO = { export type TListMembershipGroupDTO = {
permission: OrgServiceActor; permission: OrgServiceActor;
scopeData: AccessScopeData; scopeData: AccessScopeData;
selector: {
groupId: string;
};
data: { data: {
limit?: number; limit?: number;
offset?: number; offset?: number;
groupName?: string; groupName?: string;
roles: string[]; roles?: string[];
}; };
}; };
@@ -23,6 +23,7 @@ type TProjectMembershipGroupScopeFactoryDep = {
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "findOne">; membershipGroupDAL: Pick<TMembershipGroupDALFactory, "findOne">;
}; };
// TODO(simp): check for group in org
export const newProjectMembershipGroupFactory = ({ export const newProjectMembershipGroupFactory = ({
permissionService, permissionService,
orgDAL, orgDAL,
+23 -34
View File
@@ -1,20 +1,22 @@
import { AccessScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
type TDeleteOrgMembership = { type TDeleteOrgMembership = {
orgMembershipId: string; orgMembershipId: string;
orgId: string; orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">; userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
userId?: string; userId?: string;
}; };
@@ -22,11 +24,11 @@ type TDeleteOrgMemberships = {
orgMembershipIds: string[]; orgMembershipIds: string[];
orgId: string; orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipsById" | "transaction">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipsById" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "delete" | "findProjectMembershipsByUserIds">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserIds">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">; userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
userId?: string; userId?: string;
}; };
@@ -35,46 +37,41 @@ export const deleteOrgMembershipFn = async ({
orgId, orgId,
orgDAL, orgDAL,
projectMembershipDAL, projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL, projectKeyDAL,
userAliasDAL, userAliasDAL,
licenseService, licenseService,
userId userId,
membershipUserDAL
}: TDeleteOrgMembership) => { }: TDeleteOrgMembership) => {
const deletedMembership = await orgDAL.transaction(async (tx) => { const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
if (userId && orgMembership.userId === userId) { if (userId && orgMembership.actorUserId === userId) {
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken // scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
throw new BadRequestError({ message: "You cannot remove yourself from an organization" }); throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
} }
if (!orgMembership.userId) { const deletedUserId = orgMembership.actorUserId;
if (!deletedUserId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership; return orgMembership;
} }
await userAliasDAL.delete( await userAliasDAL.delete(
{ {
userId: orgMembership.userId, userId: deletedUserId,
orgId orgId
}, },
tx tx
); );
await projectUserAdditionalPrivilegeDAL.delete(
{
userId: orgMembership.userId
},
tx
);
// Get all the project memberships of the user in the organization // Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId); const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, deletedUserId);
// Delete all the project memberships of the user in the organization // Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete( await membershipUserDAL.delete(
{ {
scope: AccessScope.Project,
$in: { $in: {
id: projectMemberships.map((membership) => membership.id) id: projectMemberships.map((membership) => membership.id)
} }
@@ -87,7 +84,7 @@ export const deleteOrgMembershipFn = async ({
$in: { $in: {
projectId: projectMemberships.map((membership) => membership.projectId) projectId: projectMemberships.map((membership) => membership.projectId)
}, },
receiverId: orgMembership.userId receiverId: deletedUserId
}); });
// Delete all the project keys of the user in the organization // Delete all the project keys of the user in the organization
@@ -112,18 +109,18 @@ export const deleteOrgMembershipsFn = async ({
orgId, orgId,
orgDAL, orgDAL,
projectMembershipDAL, projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL, projectKeyDAL,
userAliasDAL, userAliasDAL,
licenseService, licenseService,
userId userId,
membershipUserDAL
}: TDeleteOrgMemberships) => { }: TDeleteOrgMemberships) => {
const deletedMemberships = await orgDAL.transaction(async (tx) => { const deletedMemberships = await orgDAL.transaction(async (tx) => {
const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx); const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx);
const membershipUserIds = orgMemberships const membershipUserIds = orgMemberships
.filter((member) => Boolean(member.userId)) .filter((member) => Boolean(member.actorUserId))
.map((member) => member.userId) as string[]; .map((member) => member.actorUserId) as string[];
if (userId && membershipUserIds.includes(userId)) { if (userId && membershipUserIds.includes(userId)) {
// scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken // scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken
@@ -145,21 +142,13 @@ export const deleteOrgMembershipsFn = async ({
tx tx
); );
await projectUserAdditionalPrivilegeDAL.delete(
{
$in: {
userId: membershipUserIds
}
},
tx
);
// Get all the project memberships of the users in the organization // Get all the project memberships of the users in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds); const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds);
// Delete all the project memberships of the users in the organization // Delete all the project memberships of the users in the organization
await projectMembershipDAL.delete( await membershipUserDAL.delete(
{ {
scope: AccessScope.Project,
$in: { $in: {
id: projectMemberships.map((membership) => membership.id) id: projectMemberships.map((membership) => membership.id)
} }
@@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectUserMembershipRoleDALFactory = ReturnType<typeof projectUserMembershipRoleDALFactory>;
export const projectUserMembershipRoleDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectUserMembershipRole);
return orm;
};