mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 00:26:40 +00:00
feat: wrappped up membership-user service
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
import {
|
import {
|
||||||
CASL_ACTION_SCHEMA_ENUM,
|
CASL_ACTION_SCHEMA_ENUM,
|
||||||
CASL_ACTION_SCHEMA_NATIVE_ENUM
|
CASL_ACTION_SCHEMA_NATIVE_ENUM
|
||||||
@@ -199,6 +200,9 @@ export enum ProjectPermissionPamSessionActions {
|
|||||||
// Terminate = "terminate"
|
// Terminate = "terminate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isCustomProjectRole = (slug: string) =>
|
||||||
|
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db";
|
||||||
|
import { TSearchResourceOperator } from "@app/lib/search-resource/search";
|
||||||
|
|
||||||
|
export type TMembershipUserDALFactory = ReturnType<typeof membershipUserDALFactory>;
|
||||||
|
|
||||||
|
type TFindUserArg = {
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
tx?: Knex;
|
||||||
|
filter: Partial<{
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
userId?: string;
|
||||||
|
username: Omit<TSearchResourceOperator, "number">;
|
||||||
|
role: Omit<TSearchResourceOperator, "number">;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TGetUserByIdArg = {
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
tx?: Knex;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const membershipUserDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.Membership);
|
||||||
|
|
||||||
|
const getUserById = async ({ scopeData, tx, userId }: TGetUserByIdArg) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.Membership)
|
||||||
|
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||||
|
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
|
||||||
|
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
|
||||||
|
.leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
|
||||||
|
.where(`${TableName.Membership}.actorUserId`, userId)
|
||||||
|
.where((qb) => {
|
||||||
|
if (scopeData.scope === AccessScope.Organization) {
|
||||||
|
void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization);
|
||||||
|
} else if (scopeData.scope === AccessScope.Namespace) {
|
||||||
|
void qb
|
||||||
|
.where(`${TableName.Membership}.scope`, AccessScope.Namespace)
|
||||||
|
.where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId)
|
||||||
|
.whereNull(`${TableName.Membership}.scopeNamespaceId`);
|
||||||
|
} else if (scopeData.scope === AccessScope.Project) {
|
||||||
|
void qb
|
||||||
|
.where(`${TableName.Membership}.scope`, AccessScope.Project)
|
||||||
|
.where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.Membership))
|
||||||
|
.select(
|
||||||
|
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
|
||||||
|
db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"),
|
||||||
|
db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"),
|
||||||
|
db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.MembershipRole)
|
||||||
|
.as("membershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.MembershipRole)
|
||||||
|
.as("membershipRoleTemporaryAccessEndTime"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => MembershipsSchema.parse(el),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "membershipRoleId",
|
||||||
|
label: "roles" as const,
|
||||||
|
mapper: ({
|
||||||
|
roleSlug,
|
||||||
|
membershipRoleId,
|
||||||
|
membershipRole,
|
||||||
|
membershipRoleIsTemporary,
|
||||||
|
membershipRoleTemporaryMode,
|
||||||
|
membershipRoleTemporaryRange,
|
||||||
|
membershipRoleTemporaryAccessEndTime,
|
||||||
|
membershipRoleTemporaryAccessStartTime,
|
||||||
|
membershipRoleCreatedAt,
|
||||||
|
membershipRoleUpdatedAt
|
||||||
|
}) => ({
|
||||||
|
id: membershipRoleId,
|
||||||
|
role: membershipRole,
|
||||||
|
customRoleSlug: roleSlug,
|
||||||
|
temporaryRange: membershipRoleTemporaryRange,
|
||||||
|
temporaryMode: membershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: membershipRoleIsTemporary,
|
||||||
|
createdAt: membershipRoleCreatedAt,
|
||||||
|
updatedAt: membershipRoleUpdatedAt
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data?.[0];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "MembershipGetByUserId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findUsers = async ({ scopeData, tx, filter }: TFindUserArg) => {
|
||||||
|
try {
|
||||||
|
const paginatedUsers = (tx || db.replicaNode())(TableName.Membership)
|
||||||
|
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||||
|
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
|
||||||
|
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
|
||||||
|
.leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`)
|
||||||
|
.distinct(`${TableName.Membership}.id`)
|
||||||
|
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
|
||||||
|
.where((qb) => {
|
||||||
|
if (scopeData.scope === AccessScope.Organization) {
|
||||||
|
void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization);
|
||||||
|
} else if (scopeData.scope === AccessScope.Namespace) {
|
||||||
|
void qb
|
||||||
|
.where(`${TableName.Membership}.scope`, AccessScope.Namespace)
|
||||||
|
.where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId)
|
||||||
|
.whereNull(`${TableName.Membership}.scopeNamespaceId`);
|
||||||
|
} else if (scopeData.scope === AccessScope.Project) {
|
||||||
|
void qb
|
||||||
|
.where(`${TableName.Membership}.scope`, AccessScope.Project)
|
||||||
|
.where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filter.limit) void paginatedUsers.limit(filter.limit);
|
||||||
|
if (filter.offset) void paginatedUsers.offset(filter.offset);
|
||||||
|
|
||||||
|
if (filter.username || filter.role) {
|
||||||
|
buildKnexFilterForSearchResource(
|
||||||
|
paginatedUsers,
|
||||||
|
{
|
||||||
|
username: filter.username!,
|
||||||
|
role: filter.role!
|
||||||
|
},
|
||||||
|
(attr) => {
|
||||||
|
switch (attr) {
|
||||||
|
case "role":
|
||||||
|
return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`];
|
||||||
|
case "username":
|
||||||
|
return `${TableName.Users}.name`;
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Invalid ${String(attr)} provided` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.Membership)
|
||||||
|
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||||
|
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
|
||||||
|
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
|
||||||
|
.leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`)
|
||||||
|
.distinct(`${TableName.Membership}.id`)
|
||||||
|
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
|
||||||
|
.whereIn(`${TableName.Membership}.id`, paginatedUsers)
|
||||||
|
.select(selectAllTableCols(TableName.Membership))
|
||||||
|
.select(
|
||||||
|
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
|
||||||
|
db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"),
|
||||||
|
db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"),
|
||||||
|
db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"),
|
||||||
|
db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"),
|
||||||
|
db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessStartTime")
|
||||||
|
.withSchema(TableName.MembershipRole)
|
||||||
|
.as("membershipRoleTemporaryAccessStartTime"),
|
||||||
|
db
|
||||||
|
.ref("temporaryAccessEndTime")
|
||||||
|
.withSchema(TableName.MembershipRole)
|
||||||
|
.as("membershipRoleTemporaryAccessEndTime"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt")
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.raw(
|
||||||
|
`count(${TableName.Membership}."actorUserId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => MembershipsSchema.parse(el),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "membershipRoleId",
|
||||||
|
label: "roles" as const,
|
||||||
|
mapper: ({
|
||||||
|
roleSlug,
|
||||||
|
membershipRoleId,
|
||||||
|
membershipRole,
|
||||||
|
membershipRoleIsTemporary,
|
||||||
|
membershipRoleTemporaryMode,
|
||||||
|
membershipRoleTemporaryRange,
|
||||||
|
membershipRoleTemporaryAccessEndTime,
|
||||||
|
membershipRoleTemporaryAccessStartTime,
|
||||||
|
membershipRoleCreatedAt,
|
||||||
|
membershipRoleUpdatedAt
|
||||||
|
}) => ({
|
||||||
|
id: membershipRoleId,
|
||||||
|
role: membershipRole,
|
||||||
|
customRoleSlug: roleSlug,
|
||||||
|
temporaryRange: membershipRoleTemporaryRange,
|
||||||
|
temporaryMode: membershipRoleTemporaryMode,
|
||||||
|
temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime,
|
||||||
|
isTemporary: membershipRoleIsTemporary,
|
||||||
|
createdAt: membershipRoleCreatedAt,
|
||||||
|
updatedAt: membershipRoleUpdatedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "MembershipfindUser" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...orm, findUsers, getUserById };
|
||||||
|
};
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
import { ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { SearchResourceOperators } from "@app/lib/search-resource/search";
|
||||||
|
|
||||||
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
|
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
|
||||||
|
import { TRoleDALFactory } from "../role/role-dal";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TMembershipUserDALFactory } from "./membership-user-dal";
|
||||||
|
import {
|
||||||
|
TCreateMembershipUserDTO,
|
||||||
|
TDeleteMembershipUserDTO,
|
||||||
|
TGetMembershipUserByUserIdDTO,
|
||||||
|
TListMembershipUserDTO,
|
||||||
|
TMembershipUserScopeFactory,
|
||||||
|
TUpdateMembershipUserDTO
|
||||||
|
} from "./membership-user-types";
|
||||||
|
|
||||||
|
type TMembershipUserServiceFactoryDep = {
|
||||||
|
membershipUserDAL: TMembershipUserDALFactory;
|
||||||
|
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "insertMany" | "delete">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
roleDAL: Pick<TRoleDALFactory, "find">;
|
||||||
|
userDAL: Pick<
|
||||||
|
TUserDALFactory,
|
||||||
|
| "findUserByUsername"
|
||||||
|
| "find"
|
||||||
|
| "transaction"
|
||||||
|
| "create"
|
||||||
|
| "createUserEncryption"
|
||||||
|
| "findUserByEmail"
|
||||||
|
| "findUserEncKeyByUserId"
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMembershipUserServiceFactory = ReturnType<typeof membershipUserServiceFactory>;
|
||||||
|
|
||||||
|
export const membershipUserServiceFactory = ({
|
||||||
|
membershipUserDAL,
|
||||||
|
roleDAL,
|
||||||
|
licenseService,
|
||||||
|
membershipRoleDAL,
|
||||||
|
userDAL
|
||||||
|
}: TMembershipUserServiceFactoryDep) => {
|
||||||
|
const scopeFactory: Record<string, TMembershipUserScopeFactory> = {};
|
||||||
|
|
||||||
|
const $getUsers = async (usernames: string[]) => {
|
||||||
|
const existingUsers = await userDAL.find({ $in: { username: usernames } });
|
||||||
|
if (existingUsers.length !== usernames.length) {
|
||||||
|
const newUserEmails = usernames.filter(
|
||||||
|
(inviteeEmail) => !existingUsers.find((el) => el.username === inviteeEmail)
|
||||||
|
);
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
for await (const inviteeEmail of newUserEmails) {
|
||||||
|
const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||||
|
let inviteeUser =
|
||||||
|
usersByUsername?.length > 1
|
||||||
|
? usersByUsername.find((el) => el.username === inviteeEmail)
|
||||||
|
: usersByUsername?.[0];
|
||||||
|
|
||||||
|
// if the user doesn't exist we create the user with the email
|
||||||
|
if (!inviteeUser) {
|
||||||
|
// TODO(carlos): will be removed once the function receives usernames instead of emails
|
||||||
|
const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx);
|
||||||
|
if (usersByEmail?.length === 1) {
|
||||||
|
[inviteeUser] = usersByEmail;
|
||||||
|
} else {
|
||||||
|
inviteeUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
isAccepted: false,
|
||||||
|
email: inviteeEmail,
|
||||||
|
username: inviteeEmail,
|
||||||
|
authMethods: [AuthMethod.EMAIL],
|
||||||
|
isGhost: false
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUsers.push(inviteeUser);
|
||||||
|
const inviteeUserId = inviteeUser?.id;
|
||||||
|
const existingEncrytionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
|
||||||
|
|
||||||
|
// when user is missing the encrytion keys
|
||||||
|
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
|
||||||
|
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
|
||||||
|
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
|
||||||
|
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
|
||||||
|
await userDAL.createUserEncryption(
|
||||||
|
{
|
||||||
|
userId: inviteeUserId,
|
||||||
|
encryptionVersion: 2
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return existingUsers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMembership = async (dto: TCreateMembershipUserDTO) => {
|
||||||
|
const { scopeData, data } = dto;
|
||||||
|
const factory = scopeFactory[scopeData.scope];
|
||||||
|
|
||||||
|
const hasOnePermanentRole = data.roles.some((el) => el.isTemporary);
|
||||||
|
if (hasOnePermanentRole) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "User must have atleast one permanent role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const isInvalidTemporaryRole = data.roles.some((el) => {
|
||||||
|
if (el.isTemporary) {
|
||||||
|
if (!el.temporaryAccessStartTime || !el.temporaryRange) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (isInvalidTemporaryRole) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Temporary role must have access start time and range"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData);
|
||||||
|
const users = await $getUsers(dto.data.usernames);
|
||||||
|
const existingMemberships = await membershipUserDAL.find({
|
||||||
|
scope: scopeData.scope,
|
||||||
|
...scopeDatabaseFields,
|
||||||
|
$in: {
|
||||||
|
actorUserId: users.map((el) => el.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// TODO(simp): do we really do this - check it out
|
||||||
|
if (existingMemberships.length === users.length) return { memberships: [] };
|
||||||
|
|
||||||
|
await factory.onCreateMembershipUserGuard(dto);
|
||||||
|
const newMembershipUsers = users.filter((user) => !existingMemberships?.find((el) => el.actorUserId === user.id));
|
||||||
|
const newMemberships = newMembershipUsers.map((user) => ({
|
||||||
|
scope: scopeData.scope,
|
||||||
|
...scopeDatabaseFields,
|
||||||
|
actorUserId: user.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role));
|
||||||
|
const hasCustomRole = customInputRoles.length > 0;
|
||||||
|
if (hasCustomRole) {
|
||||||
|
const plan = await licenseService.getPlan(scopeData.orgId);
|
||||||
|
if (!plan?.rbac)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeField = factory.getScopeField(dto.scopeData);
|
||||||
|
const customRoles = hasCustomRole
|
||||||
|
? await roleDAL.find({
|
||||||
|
[scopeField.key]: scopeField.value,
|
||||||
|
$in: { slug: customInputRoles.map(({ role }) => role) }
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
if (customRoles.length !== customInputRoles.length) {
|
||||||
|
throw new NotFoundError({ message: "One or more custom roles not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
|
const membershipDoc = await membershipUserDAL.transaction(async (tx) => {
|
||||||
|
const docs = await membershipUserDAL.insertMany(newMemberships, tx);
|
||||||
|
|
||||||
|
const roleDocs: TMembershipRolesInsert[] = [];
|
||||||
|
docs.forEach((membership) => {
|
||||||
|
data.roles.forEach((membershipRole) => {
|
||||||
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]);
|
||||||
|
if (membershipRole.isTemporary) {
|
||||||
|
const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null;
|
||||||
|
roleDocs.push({
|
||||||
|
membershipId: membership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[membershipRole.role]
|
||||||
|
? customRolesGroupBySlug[membershipRole.role][0].id
|
||||||
|
: null,
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryMode: TemporaryPermissionMode.Relative,
|
||||||
|
temporaryRange: membershipRole.temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string),
|
||||||
|
temporaryAccessEndTime: new Date(
|
||||||
|
new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
roleDocs.push({
|
||||||
|
membershipId: membership.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[membershipRole.role]
|
||||||
|
? customRolesGroupBySlug[membershipRole.role][0].id
|
||||||
|
: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await membershipRoleDAL.insertMany(roleDocs, tx);
|
||||||
|
return docs;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { memberships: membershipDoc };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMembership = async (dto: TUpdateMembershipUserDTO) => {
|
||||||
|
const { scopeData, data } = dto;
|
||||||
|
const factory = scopeFactory[scopeData.scope];
|
||||||
|
|
||||||
|
await factory.onUpdateMembershipUserGuard(dto);
|
||||||
|
|
||||||
|
const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role));
|
||||||
|
const hasCustomRole = customInputRoles.length > 0;
|
||||||
|
if (hasCustomRole) {
|
||||||
|
const plan = await licenseService.getPlan(scopeData.orgId);
|
||||||
|
if (!plan?.rbac)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOnePermanentRole = data.roles.some((el) => el.isTemporary);
|
||||||
|
if (hasOnePermanentRole) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "User must have atleast one permanent role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const isInvalidTemporaryRole = data.roles.some((el) => {
|
||||||
|
if (el.isTemporary) {
|
||||||
|
if (!el.temporaryAccessStartTime || !el.temporaryRange) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (isInvalidTemporaryRole) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Temporary role must have access start time and range"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData);
|
||||||
|
const existingMembership = await membershipUserDAL.findOne({
|
||||||
|
scope: scopeData.scope,
|
||||||
|
...scopeDatabaseFields,
|
||||||
|
actorUserId: dto.selector.actorId
|
||||||
|
});
|
||||||
|
if (!existingMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "User doesn't have membership"
|
||||||
|
});
|
||||||
|
|
||||||
|
const scopeField = factory.getScopeField(dto.scopeData);
|
||||||
|
const customRoles = hasCustomRole
|
||||||
|
? await roleDAL.find({
|
||||||
|
[scopeField.key]: scopeField.value,
|
||||||
|
$in: { slug: customInputRoles.map(({ role }) => role) }
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
if (customRoles.length !== customInputRoles.length) {
|
||||||
|
throw new NotFoundError({ message: "One or more custom roles not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
|
||||||
|
|
||||||
|
const membershipDoc = await membershipUserDAL.transaction(async (tx) => {
|
||||||
|
const doc = await membershipUserDAL.updateById(
|
||||||
|
existingMembership.id,
|
||||||
|
{
|
||||||
|
isActive: data.isActive
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const roleDocs: TMembershipRolesInsert[] = [];
|
||||||
|
data.roles.forEach((membershipRole) => {
|
||||||
|
const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]);
|
||||||
|
if (membershipRole.isTemporary) {
|
||||||
|
const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null;
|
||||||
|
roleDocs.push({
|
||||||
|
membershipId: doc.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[membershipRole.role]
|
||||||
|
? customRolesGroupBySlug[membershipRole.role][0].id
|
||||||
|
: null,
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryMode: TemporaryPermissionMode.Relative,
|
||||||
|
temporaryRange: membershipRole.temporaryRange,
|
||||||
|
temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string),
|
||||||
|
temporaryAccessEndTime: new Date(
|
||||||
|
new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
roleDocs.push({
|
||||||
|
membershipId: doc.id,
|
||||||
|
role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role,
|
||||||
|
customRoleId: customRolesGroupBySlug[membershipRole.role]
|
||||||
|
? customRolesGroupBySlug[membershipRole.role][0].id
|
||||||
|
: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await membershipRoleDAL.delete(
|
||||||
|
{
|
||||||
|
membershipId: doc.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await membershipRoleDAL.insertMany(roleDocs, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { memberships: membershipDoc };
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMembership = async (dto: TDeleteMembershipUserDTO) => {
|
||||||
|
const { scopeData } = dto;
|
||||||
|
const factory = scopeFactory[scopeData.scope];
|
||||||
|
|
||||||
|
const { actorIdOfDeletor } = await factory.onDeleteMembershipUserGuard(dto);
|
||||||
|
|
||||||
|
const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData);
|
||||||
|
const existingMembership = await membershipUserDAL.findOne({
|
||||||
|
scope: scopeData.scope,
|
||||||
|
...scopeDatabaseFields,
|
||||||
|
actorUserId: dto.selector.actorId
|
||||||
|
});
|
||||||
|
if (!existingMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "User doesn't have membership"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMembership.actorUserId === actorIdOfDeletor)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "You can delete you own membership"
|
||||||
|
});
|
||||||
|
|
||||||
|
const membershipDoc = await membershipUserDAL.transaction(async (tx) => {
|
||||||
|
await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx);
|
||||||
|
const doc = await membershipUserDAL.deleteById(existingMembership.id, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
return { membership: membershipDoc };
|
||||||
|
};
|
||||||
|
|
||||||
|
const listMemberships = async (dto: TListMembershipUserDTO) => {
|
||||||
|
const { scopeData } = dto;
|
||||||
|
const factory = scopeFactory[scopeData.scope];
|
||||||
|
|
||||||
|
await factory.onListMembershipUserGuard(dto);
|
||||||
|
const memberships = await membershipUserDAL.findUsers({
|
||||||
|
scopeData,
|
||||||
|
filter: {
|
||||||
|
limit: dto.data.limit,
|
||||||
|
offset: dto.data.offset,
|
||||||
|
username: dto.data.username,
|
||||||
|
role: dto.data.roles.length
|
||||||
|
? {
|
||||||
|
[SearchResourceOperators.$in]: dto.data.roles
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return memberships;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMembershipByUserId = async (dto: TGetMembershipUserByUserIdDTO) => {
|
||||||
|
const { scopeData, selector } = dto;
|
||||||
|
const factory = scopeFactory[scopeData.scope];
|
||||||
|
|
||||||
|
await factory.onGetMembershipUserByUserIdGuard(dto);
|
||||||
|
const membership = await membershipUserDAL.getUserById({
|
||||||
|
scopeData,
|
||||||
|
userId: selector.actorId
|
||||||
|
});
|
||||||
|
if (!membership) throw new NotFoundError({ message: `User membership not found` });
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createMembership,
|
||||||
|
updateMembership,
|
||||||
|
deleteMembership,
|
||||||
|
listMemberships,
|
||||||
|
getMembershipByUserId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
export interface TMembershipUserScopeFactory {
|
||||||
|
onCreateMembershipUserGuard: (arg: TCreateMembershipUserDTO) => Promise<void>;
|
||||||
|
onCreateMembershipComplete: (arg: { id: string; email: string }[]) => Promise<void>;
|
||||||
|
|
||||||
|
onUpdateMembershipUserGuard: (arg: TUpdateMembershipUserDTO) => Promise<void>;
|
||||||
|
onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO) => Promise<{ actorIdOfDeletor: string }>;
|
||||||
|
onListMembershipUserGuard: (arg: TListMembershipUserDTO) => Promise<void>;
|
||||||
|
onGetMembershipUserByUserIdGuard: (arg: TGetMembershipUserByUserIdDTO) => Promise<void>;
|
||||||
|
getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string };
|
||||||
|
getScopeDatabaseFields: (scope: AccessScopeData) => {
|
||||||
|
scopeOrgId: string;
|
||||||
|
scopeNamespaceId?: string | null;
|
||||||
|
scopeProjectId?: string | null;
|
||||||
|
};
|
||||||
|
isCustomRole: (role: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateMembershipUserDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
data: {
|
||||||
|
usernames: string[];
|
||||||
|
roles: {
|
||||||
|
role: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryMode?: TemporaryPermissionMode.Relative;
|
||||||
|
temporaryRange?: string;
|
||||||
|
temporaryAccessStartTime?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateMembershipUserDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
selector: {
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
data: {
|
||||||
|
isActive?: boolean;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
|
roles: {
|
||||||
|
role: string;
|
||||||
|
isTemporary: boolean;
|
||||||
|
temporaryMode?: TemporaryPermissionMode.Relative;
|
||||||
|
temporaryRange?: string;
|
||||||
|
temporaryAccessStartTime?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListMembershipUserDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
selector: {
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
data: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
username?: string;
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteMembershipUserDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
selector: {
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetMembershipUserByUserIdDTO = {
|
||||||
|
permission: OrgServiceActor;
|
||||||
|
scopeData: AccessScopeData;
|
||||||
|
selector: {
|
||||||
|
actorId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { AccessScope } from "@app/db/schemas";
|
||||||
|
import { InternalServerError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TMembershipUserScopeFactory } from "../membership-user-types";
|
||||||
|
|
||||||
|
type TNamespaceMembershipUserScopeFactoryDep = Record<string, never>;
|
||||||
|
|
||||||
|
export const newNamespaceMembershipUserFactory = (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
deps: TNamespaceMembershipUserScopeFactoryDep
|
||||||
|
): TMembershipUserScopeFactory => {
|
||||||
|
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
|
||||||
|
if (dto.scope === AccessScope.Namespace) {
|
||||||
|
return { key: "namespaceId" as const, value: dto.namespaceId };
|
||||||
|
}
|
||||||
|
throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => {
|
||||||
|
if (dto.scope === AccessScope.Namespace) {
|
||||||
|
return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId };
|
||||||
|
}
|
||||||
|
throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user isCustomRole not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user create not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user create complete not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user update not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user delete not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user list not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] =
|
||||||
|
async () => {
|
||||||
|
throw new InternalServerError({ message: "Namespace membership user get by user id not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreateMembershipUserGuard,
|
||||||
|
onCreateMembershipComplete,
|
||||||
|
onUpdateMembershipUserGuard,
|
||||||
|
onDeleteMembershipUserGuard,
|
||||||
|
onListMembershipUserGuard,
|
||||||
|
onGetMembershipUserByUserIdGuard,
|
||||||
|
getScopeField,
|
||||||
|
getScopeDatabaseFields,
|
||||||
|
isCustomRole
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { AccessScope } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { InternalServerError } from "@app/lib/errors";
|
||||||
|
import { isCustomOrgRole } from "@app/services/org/org-role-fns";
|
||||||
|
|
||||||
|
import { TMembershipUserScopeFactory } from "../membership-user-types";
|
||||||
|
|
||||||
|
type TOrgMembershipUserScopeFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newOrgMembershipUserFactory = ({
|
||||||
|
permissionService
|
||||||
|
}: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => {
|
||||||
|
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
|
||||||
|
if (dto.scope === AccessScope.Organization) {
|
||||||
|
return { key: "orgId" as const, value: dto.orgId };
|
||||||
|
}
|
||||||
|
throw new InternalServerError({ message: "Invalid scope provided for the org factory" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => {
|
||||||
|
if (dto.scope === AccessScope.Organization) {
|
||||||
|
return { scopeOrgId: dto.orgId };
|
||||||
|
}
|
||||||
|
throw new InternalServerError({ message: "Invalid scope provided for the org factory" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role);
|
||||||
|
|
||||||
|
const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
dto.permission.type,
|
||||||
|
dto.permission.id,
|
||||||
|
dto.permission.orgId,
|
||||||
|
dto.permission.authMethod,
|
||||||
|
dto.permission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => {
|
||||||
|
// TODO(simp): fix this
|
||||||
|
throw new InternalServerError({ message: "Org membership user create complete not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
dto.permission.type,
|
||||||
|
dto.permission.id,
|
||||||
|
dto.permission.orgId,
|
||||||
|
dto.permission.authMethod,
|
||||||
|
dto.permission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
dto.permission.type,
|
||||||
|
dto.permission.id,
|
||||||
|
dto.permission.orgId,
|
||||||
|
dto.permission.authMethod,
|
||||||
|
dto.permission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
|
||||||
|
return { actorIdOfDeletor: dto.permission.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
dto.permission.type,
|
||||||
|
dto.permission.id,
|
||||||
|
dto.permission.orgId,
|
||||||
|
dto.permission.authMethod,
|
||||||
|
dto.permission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async (
|
||||||
|
dto
|
||||||
|
) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
dto.permission.type,
|
||||||
|
dto.permission.id,
|
||||||
|
dto.permission.orgId,
|
||||||
|
dto.permission.authMethod,
|
||||||
|
dto.permission.orgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreateMembershipUserGuard,
|
||||||
|
onCreateMembershipComplete,
|
||||||
|
onUpdateMembershipUserGuard,
|
||||||
|
onDeleteMembershipUserGuard,
|
||||||
|
onListMembershipUserGuard,
|
||||||
|
onGetMembershipUserByUserIdGuard,
|
||||||
|
getScopeField,
|
||||||
|
getScopeDatabaseFields,
|
||||||
|
isCustomRole
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { AccessScope, ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import {
|
||||||
|
isCustomProjectRole,
|
||||||
|
ProjectPermissionMemberActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { InternalServerError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TMembershipUserScopeFactory } from "../membership-user-types";
|
||||||
|
|
||||||
|
type TProjectMembershipUserScopeFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newProjectMembershipUserFactory = ({
|
||||||
|
permissionService
|
||||||
|
}: TProjectMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => {
|
||||||
|
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
|
||||||
|
if (dto.scope === AccessScope.Project) {
|
||||||
|
return { key: "projectId" as const, value: dto.projectId };
|
||||||
|
}
|
||||||
|
throw new InternalServerError({ message: "Invalid scope provided for the project factory" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => {
|
||||||
|
if (dto.scope === AccessScope.Project) {
|
||||||
|
return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId };
|
||||||
|
}
|
||||||
|
throw new InternalServerError({ message: "Invalid scope provided for the project factory" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role);
|
||||||
|
|
||||||
|
// TODO(simp): do rest of the shouldUsePrivilegeV2 check
|
||||||
|
const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => {
|
||||||
|
const scope = getScopeField(dto.scopeData);
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: dto.permission.type,
|
||||||
|
actorId: dto.permission.id,
|
||||||
|
actionProjectType: ActionProjectType.Any,
|
||||||
|
actorAuthMethod: dto.permission.authMethod,
|
||||||
|
projectId: scope.value,
|
||||||
|
actorOrgId: dto.permission.orgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => {
|
||||||
|
throw new InternalServerError({ message: "Project membership user create complete not implemented" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => {
|
||||||
|
const scope = getScopeField(dto.scopeData);
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: dto.permission.type,
|
||||||
|
actorId: dto.permission.id,
|
||||||
|
actionProjectType: ActionProjectType.Any,
|
||||||
|
actorAuthMethod: dto.permission.authMethod,
|
||||||
|
projectId: scope.value,
|
||||||
|
actorOrgId: dto.permission.orgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => {
|
||||||
|
const scope = getScopeField(dto.scopeData);
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: dto.permission.type,
|
||||||
|
actorId: dto.permission.id,
|
||||||
|
actionProjectType: ActionProjectType.Any,
|
||||||
|
actorAuthMethod: dto.permission.authMethod,
|
||||||
|
projectId: scope.value,
|
||||||
|
actorOrgId: dto.permission.orgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
|
||||||
|
return { actorIdOfDeletor: dto.permission.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => {
|
||||||
|
const scope = getScopeField(dto.scopeData);
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: dto.permission.type,
|
||||||
|
actorId: dto.permission.id,
|
||||||
|
actionProjectType: ActionProjectType.Any,
|
||||||
|
actorAuthMethod: dto.permission.authMethod,
|
||||||
|
projectId: scope.value,
|
||||||
|
actorOrgId: dto.permission.orgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async (
|
||||||
|
dto
|
||||||
|
) => {
|
||||||
|
const scope = getScopeField(dto.scopeData);
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: dto.permission.type,
|
||||||
|
actorId: dto.permission.id,
|
||||||
|
actionProjectType: ActionProjectType.Any,
|
||||||
|
actorAuthMethod: dto.permission.authMethod,
|
||||||
|
projectId: scope.value,
|
||||||
|
actorOrgId: dto.permission.orgId
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onCreateMembershipUserGuard,
|
||||||
|
onCreateMembershipComplete,
|
||||||
|
onUpdateMembershipUserGuard,
|
||||||
|
onDeleteMembershipUserGuard,
|
||||||
|
onListMembershipUserGuard,
|
||||||
|
onGetMembershipUserByUserIdGuard,
|
||||||
|
getScopeField,
|
||||||
|
getScopeDatabaseFields,
|
||||||
|
isCustomRole
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TMembershipRoleDALFactory = ReturnType<typeof membershipRoleDALFactory>;
|
||||||
|
|
||||||
|
export const membershipRoleDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.MembershipRole);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { TMembershipDALFactory } from "./membership-dal";
|
|
||||||
|
|
||||||
type TMembershipServiceFactoryDep = {
|
|
||||||
membershipDAL: TMembershipDALFactory;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TMembershipServiceFactory = ReturnType<typeof membershipServiceFactory>;
|
|
||||||
|
|
||||||
export const membershipServiceFactory = ({ membershipDAL }: TMembershipServiceFactoryDep) => {
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user