feat: almost done with most ts stuff

This commit is contained in:
=
2025-10-03 19:50:47 +05:30
parent 44c9575a3d
commit c2490380f0
28 changed files with 551 additions and 1432 deletions

View File

@@ -58,7 +58,6 @@ type TSecretApprovalRequestServiceFactoryDep = {
"create" | "find" | "findOne" | "transaction" | "delete"
>;
groupDAL: Pick<TGroupDALFactory, "findAllGroupPossibleMembers">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<
TUserDALFactory,

View File

@@ -25,13 +25,12 @@ import { TMembershipRoleDALFactory } from "@app/services/membership/membership-r
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 { deleteOrgMembershipFn } from "@app/services/org/org-fns";
import { deleteOrgMembershipsFn } from "@app/services/org/org-fns";
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
import { OrgAuthMethod } from "@app/services/org/org-types";
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 { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -65,7 +64,6 @@ type TScimServiceFactoryDep = {
>;
membershipUserDAL: TMembershipUserDALFactory;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId">;
groupDAL: Pick<
TGroupDALFactory,
| "create"
@@ -78,7 +76,7 @@ type TScimServiceFactoryDep = {
| "update"
>;
membershipGroupDAL: Pick<TMembershipGroupDALFactory, "find">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create" | "update">;
membershipRoleDAL: TMembershipRoleDALFactory;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
| "find"
@@ -104,7 +102,6 @@ export const scimServiceFactory = ({
userAliasDAL,
orgDAL,
projectDAL,
projectMembershipDAL,
groupDAL,
userGroupMembershipDAL,
projectKeyDAL,
@@ -667,15 +664,16 @@ export const scimServiceFactory = ({
});
}
await deleteOrgMembershipFn({
orgMembershipId: membership.id,
await deleteOrgMembershipsFn({
orgMembershipIds: [membership.id],
orgId: membership.scopeOrgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
membershipUserDAL
membershipUserDAL,
membershipRoleDAL,
userGroupMembershipDAL
});
return {}; // intentionally return empty object upon success

View File

@@ -171,6 +171,7 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
@@ -184,6 +185,7 @@ import { folderCommitChangesDALFactory } from "@app/services/folder-commit-chang
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { identityDALFactory } from "@app/services/identity/identity-dal";
import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
@@ -209,6 +211,7 @@ import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/i
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal";
import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service";
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
@@ -340,9 +343,6 @@ import { initializeOauthConfigSync } from "./v1/sso-router";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
import { registerV4Routes } from "./v4";
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
@@ -560,6 +560,8 @@ export const registerRoutes = async (
projectDAL
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL });
const membershipUserService = membershipUserServiceFactory({
licenseService,
membershipRoleDAL,
@@ -567,7 +569,13 @@ export const registerRoutes = async (
orgDAL,
permissionService,
roleDAL,
userDAL
userDAL,
projectDAL,
projectKeyDAL,
smtpService,
tokenService,
userAliasDAL,
userGroupMembershipDAL
});
const membershipIdentityService = membershipIdentityServiceFactory({
@@ -581,7 +589,9 @@ export const registerRoutes = async (
const membershipGroupService = membershipGroupServiceFactory({
membershipGroupDAL,
membershipRoleDAL,
roleDAL
roleDAL,
permissionService,
orgDAL
});
const roleService = roleServiceFactory({
@@ -663,7 +673,6 @@ export const registerRoutes = async (
userDAL,
secretApprovalRequestDAL
});
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL });
const samlService = samlConfigServiceFactory({
identityMetadataDAL,
@@ -746,7 +755,6 @@ export const registerRoutes = async (
userAliasDAL,
orgDAL,
projectDAL,
projectMembershipDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
@@ -882,7 +890,8 @@ export const registerRoutes = async (
reminderService,
membershipRoleDAL,
membershipUserDAL,
roleDAL
roleDAL,
userGroupMembershipDAL
});
const signupService = authSignupServiceFactory({
tokenService,
@@ -970,7 +979,6 @@ export const registerRoutes = async (
projectMembershipDAL,
projectDAL,
permissionService,
orgDAL,
userDAL,
userGroupMembershipDAL,
smtpService,
@@ -979,7 +987,9 @@ export const registerRoutes = async (
secretReminderRecipientsDAL,
licenseService,
notificationService,
membershipDAL
membershipUserDAL,
additionalPrivilegeDAL,
membershipRoleDAL
});
const projectKeyService = projectKeyServiceFactory({
@@ -1438,7 +1448,9 @@ export const registerRoutes = async (
projectDAL,
userDAL,
accessApprovalRequestDAL,
accessApprovalRequestReviewerDAL
accessApprovalRequestReviewerDAL,
additionalPrivilegeDAL,
membershipUserDAL
});
const accessApprovalRequestService = accessApprovalRequestServiceFactory({
@@ -1456,7 +1468,8 @@ export const registerRoutes = async (
groupDAL,
microsoftTeamsService,
projectMicrosoftTeamsConfigDAL,
notificationService
notificationService,
additionalPrivilegeDAL
});
const secretReplicationService = secretReplicationServiceFactory({

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
import { AccessScope, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { inviteUserRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -23,13 +23,6 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
.array()
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
organizationId: z.string().trim(),
projects: z
.object({
id: z.string(),
projectRoleSlug: z.string().array().default([ProjectMembershipRole.Member])
})
.array()
.optional(),
organizationRoleSlug: z.string().default(OrgMembershipRole.Member)
}),
response: {
@@ -50,15 +43,16 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return;
const { signupTokens: completeInviteLinks } = await server.services.org.inviteUserToOrganization({
orgId: req.body.organizationId,
actor: req.permission.type,
actorId: req.permission.id,
inviteeEmails: req.body.inviteeEmails,
projects: req.body.projects,
organizationRoleSlug: req.body.organizationRoleSlug,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
const { signUpTokens: completeInviteLinks } = await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
usernames: req.body.inviteeEmails,
roles: [{ isTemporary: false, role: req.body.organizationRoleSlug }]
}
});
await server.services.telemetry.sendPostHogEvents({

View File

@@ -435,13 +435,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
event: {
type: EventType.REMOVE_PROJECT_MEMBER,
metadata: {
userId: membership.userId,
userId: membership.actorUserId as string,
email: ""
}
}
});
}
return { memberships };
return {
memberships: memberships.map((el) => ({
...el,
userId: el.actorUserId as string,
projectId: req.params.projectId
}))
};
}
});
@@ -534,7 +540,13 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
actor: req.permission.type,
projectId: req.params.projectId
});
return { membership };
return {
membership: {
...membership,
userId: membership.actorUserId as string,
projectId: req.params.projectId
}
};
}
});
};

View File

@@ -1,10 +1,12 @@
import { z } from "zod";
import {
AccessScope,
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
ProjectUserMembershipRolesSchema
ProjectUserMembershipRolesSchema,
TemporaryPermissionMode
} from "@app/db/schemas";
import { ApiDocsTags, ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
@@ -14,7 +16,6 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
@@ -56,7 +57,7 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryMode: z
.nativeEnum(ProjectUserMembershipTemporaryMode)
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryRange: z
.string()
@@ -82,16 +83,22 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const identityMembership = await server.services.identityProject.createProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
roles: roles || [{ role }]
const { membership } = await server.services.membershipIdentity.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
identityId: req.params.identityId,
roles: roles || [{ role, isTemporary: false }]
}
});
return { identityMembership };
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
@@ -130,7 +137,7 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(ProjectUserMembershipTemporaryMode)
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
@@ -153,16 +160,24 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod
}
},
handler: async (req) => {
const roles = await server.services.identityProject.updateProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId,
roles: req.body.roles
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
return { roles };
return {
roles: membership.roles.map((el) => ({ ...el, projectMembershipId: membership.id }))
};
}
});
@@ -193,15 +208,21 @@ export const registerDeprecatedIdentityProjectRouter = async (server: FastifyZod
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.deleteProjectIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
projectId: req.params.projectId
const { membership } = await server.services.membershipIdentity.deleteMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { AccessScope, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -51,20 +51,17 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
const { projectMemberships: memberships } = await server.services.org.inviteUserToOrganization({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
inviteeEmails: usernamesAndEmails,
orgId: req.permission.orgId,
organizationRoleSlug: OrgMembershipRole.NoAccess,
projects: [
{
id: req.params.projectId,
projectRoleSlug: req.body.roleSlugs || [ProjectMembershipRole.Member]
}
]
const { memberships } = await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
usernames: usernamesAndEmails,
roles: (req.body.roleSlugs || [ProjectMembershipRole.Member]).map((role) => ({ isTemporary: false, role }))
}
});
await server.services.auditLog.createAuditLog({
@@ -72,15 +69,21 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ
...req.auditLogInfo,
event: {
type: EventType.ADD_BATCH_PROJECT_MEMBER,
metadata: memberships.map(({ userId, id }) => ({
userId: userId || "",
metadata: memberships.map(({ actorUserId, id }) => ({
userId: actorUserId || "",
membershipId: id,
email: ""
}))
}
});
return { memberships };
return {
memberships: memberships.map((el) => ({
...el,
userId: el.actorUserId as string,
projectId: req.params.projectId
}))
};
}
});
@@ -143,13 +146,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ
event: {
type: EventType.REMOVE_PROJECT_MEMBER,
metadata: {
userId: membership.userId,
userId: membership.actorUserId as string,
email: ""
}
}
});
}
return { memberships };
return {
memberships: memberships.map((el) => ({
...el,
userId: el.actorUserId as string,
projectId: req.params.projectId
}))
};
}
});
};

View File

@@ -220,7 +220,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
...req.body
});
return { membership };
return {
membership: {
...membership,
role: "",
orgId: req.params.organizationId
}
};
}
});
@@ -260,7 +266,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
membershipId: req.params.membershipId,
actorOrgId: req.permission.orgId
});
return { membership };
return {
membership: {
...membership,
role: "",
orgId: req.params.organizationId
}
};
}
});
@@ -302,7 +314,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
membershipIds: req.body.membershipIds,
actorOrgId: req.permission.orgId
});
return { memberships };
return {
memberships: memberships.map((el) => ({
...el,
role: "",
orgId: req.params.organizationId
}))
};
}
});

View File

@@ -193,62 +193,62 @@ export const identityOrgDALFactory = (db: TDbClient) => {
})
.leftJoin<TIdentityUniversalAuths>(
TableName.IdentityUniversalAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAlicloudAuths>(
TableName.IdentityAliCloudAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityTokenAuth}.identityId`
)
.leftJoin<TIdentityJwtAuths>(
TableName.IdentityJwtAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityJwtAuth}.identityId`
)
.leftJoin<TIdentityLdapAuths>(
TableName.IdentityLdapAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityLdapAuth}.identityId`
)
.leftJoin<TIdentityTlsCertAuths>(
TableName.IdentityTlsCertAuth,
"paginatedIdentity.identityId",
"paginatedIdentity.actorIdentityId",
`${TableName.IdentityTlsCertAuth}.identityId`
)
.select(

View File

@@ -1,10 +1,12 @@
import { ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas";
import { AccessScope, ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
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 { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TRoleDALFactory } from "../role/role-dal";
import { TMembershipGroupDALFactory } from "./membership-group-dal";
import {
@@ -12,14 +14,18 @@ import {
TDeleteMembershipGroupDTO,
TGetMembershipGroupByGroupIdDTO,
TListMembershipGroupDTO,
TMembershipGroupScopeFactory,
TUpdateMembershipGroupDTO
} from "./membership-group-types";
import { newNamespaceMembershipGroupFactory } from "./namespace/namespace-membership-group-factory";
import { newOrgMembershipGroupFactory } from "./org/org-membership-group-factory";
import { newProjectMembershipGroupFactory } from "./project/project-membership-group-factory";
type TMembershipGroupServiceFactoryDep = {
membershipGroupDAL: TMembershipGroupDALFactory;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "insertMany" | "delete">;
roleDAL: Pick<TRoleDALFactory, "find">;
permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
};
export type TMembershipGroupServiceFactory = ReturnType<typeof membershipGroupServiceFactory>;
@@ -27,9 +33,22 @@ export type TMembershipGroupServiceFactory = ReturnType<typeof membershipGroupSe
export const membershipGroupServiceFactory = ({
membershipGroupDAL,
roleDAL,
membershipRoleDAL
membershipRoleDAL,
orgDAL,
permissionService
}: TMembershipGroupServiceFactoryDep) => {
const scopeFactory: Record<string, TMembershipGroupScopeFactory> = {};
const scopeFactory = {
[AccessScope.Organization]: newOrgMembershipGroupFactory({
orgDAL,
permissionService
}),
[AccessScope.Namespace]: newNamespaceMembershipGroupFactory({}),
[AccessScope.Project]: newProjectMembershipGroupFactory({
membershipGroupDAL,
orgDAL,
permissionService
})
};
const createMembership = async (dto: TCreateMembershipGroupDTO) => {
const { scopeData, data } = dto;

View File

@@ -1,4 +1,5 @@
import { AccessScope, ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -7,10 +8,16 @@ import { ms } from "@app/lib/ms";
import { SearchResourceOperators } from "@app/lib/search-resource/search";
import { AuthMethod } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { deleteOrgMembershipsFn } from "../org/org-fns";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TRoleDALFactory } from "../role/role-dal";
import { TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { TMembershipUserDALFactory } from "./membership-user-dal";
import {
TCreateMembershipUserDTO,
@@ -26,23 +33,20 @@ import { newProjectMembershipUserFactory } from "./project/project-membership-us
type TMembershipUserServiceFactoryDep = {
membershipUserDAL: TMembershipUserDALFactory;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "insertMany" | "delete">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findById" | "transaction">;
roleDAL: Pick<TRoleDALFactory, "find">;
userDAL: Pick<
TUserDALFactory,
| "findUserByUsername"
| "find"
| "transaction"
| "create"
| "createUserEncryption"
| "findUserByEmail"
| "findUserEncKeyByUserId"
>;
userDAL: TUserDALFactory;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getProjectPermissionByRoles" | "getOrgPermission"
>;
licenseService: TLicenseServiceFactory;
projectKeyDAL: TProjectKeyDALFactory;
userAliasDAL: TUserAliasDALFactory;
smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectDAL: TProjectDALFactory;
};
export type TMembershipUserServiceFactory = ReturnType<typeof membershipUserServiceFactory>;
@@ -50,20 +54,35 @@ export type TMembershipUserServiceFactory = ReturnType<typeof membershipUserServ
export const membershipUserServiceFactory = ({
membershipUserDAL,
roleDAL,
licenseService,
membershipRoleDAL,
userDAL,
permissionService,
orgDAL
orgDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
smtpService,
tokenService,
userGroupMembershipDAL,
projectDAL
}: TMembershipUserServiceFactoryDep) => {
const scopeFactory = {
[AccessScope.Organization]: newOrgMembershipUserFactory({
permissionService
permissionService,
licenseService,
smtpService,
orgDAL,
tokenService,
userDAL,
userGroupMembershipDAL
}),
[AccessScope.Namespace]: newNamespaceMembershipUserFactory({}),
[AccessScope.Project]: newProjectMembershipUserFactory({
orgDAL,
permissionService
permissionService,
membershipUserDAL,
projectDAL,
smtpService
})
};
@@ -157,11 +176,11 @@ export const membershipUserServiceFactory = ({
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));
await factory.onCreateMembershipUserGuard(dto, newMembershipUsers);
const newMemberships = newMembershipUsers.map((user) => ({
scope: scopeData.scope,
...scopeDatabaseFields,
@@ -230,7 +249,8 @@ export const membershipUserServiceFactory = ({
return docs;
});
return { memberships: membershipDoc };
const { signUpTokens } = await factory.onCreateMembershipComplete(dto, newMembershipUsers);
return { memberships: membershipDoc, signUpTokens };
};
const updateMembership = async (dto: TUpdateMembershipUserDTO) => {
@@ -364,10 +384,26 @@ export const membershipUserServiceFactory = ({
if (existingMembership.actorUserId === dto.permission.id)
throw new BadRequestError({
message: "You can delete you own membership"
message: "You can't delete you own membership"
});
const membershipDoc = await membershipUserDAL.transaction(async (tx) => {
if (dto.scopeData.scope === AccessScope.Organization) {
const [doc] = await deleteOrgMembershipsFn({
orgMembershipIds: [],
orgId: dto.permission.orgId,
orgDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId: dto.permission.id,
membershipUserDAL,
userGroupMembershipDAL,
membershipRoleDAL
});
return doc;
}
await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx);
const doc = await membershipUserDAL.deleteById(existingMembership.id, tx);
return doc;

View File

@@ -2,11 +2,18 @@ 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>;
onCreateMembershipUserGuard: (
arg: TCreateMembershipUserDTO,
newMembers: { id: string; email?: string | null }[]
) => Promise<void>;
onCreateMembershipComplete: (
arg: TCreateMembershipUserDTO,
newMembers: { id: string; email?: string | null }[]
) => Promise<{ signUpTokens: { email: string; link: string }[] }>;
onUpdateMembershipUserGuard: (arg: TUpdateMembershipUserDTO) => Promise<void>;
onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO) => Promise<void>;
onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO | TBulkDeleteMembershipByUsernameDTO) => Promise<void>;
onListMembershipUserGuard: (arg: TListMembershipUserDTO) => Promise<void>;
onGetMembershipUserByUserIdGuard: (arg: TGetMembershipUserByUserIdDTO) => Promise<void>;
getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string };
@@ -71,6 +78,14 @@ export type TDeleteMembershipUserDTO = {
};
};
export type TBulkDeleteMembershipByUsernameDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
data: {
usernames: string[];
};
};
export type TGetMembershipUserByUserIdDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;

View File

@@ -1,19 +1,39 @@
import { ForbiddenError } from "@casl/ability";
import { AccessScope } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
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 { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { isCustomOrgRole } from "@app/services/org/org-role-fns";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TMembershipUserScopeFactory } from "../membership-user-types";
type TOrgMembershipUserScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
userDAL: Pick<TUserDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findById">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export const newOrgMembershipUserFactory = ({
permissionService
permissionService,
tokenService,
userDAL,
orgDAL,
smtpService,
licenseService
}: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => {
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Organization) {
@@ -40,11 +60,75 @@ export const newOrgMembershipUserFactory = ({
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const plan = await licenseService.getPlan(dto.permission.orgId);
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
name: "InviteUser",
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
const org = await orgDAL.findById(dto.permission.orgId);
if (org?.authEnforced) {
throw new ForbiddenRequestError({
name: "InviteUser",
message: "Failed to invite user due to org-level auth enforced for organization"
});
}
};
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => {
// TODO(simp): fix this
throw new InternalServerError({ message: "Org membership user create complete not implemented" });
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async (
dto,
newUsers
) => {
const appCfg = getConfig();
const actorDetails =
dto.permission.type === ActorType.USER
? await userDAL.findById(dto.permission.id)
: {
firstName: "Platform Identity",
email: "identity"
};
const signUpTokens: { email: string; link: string }[] = [];
const orgDetails = await orgDAL.findById(dto.permission.orgId);
await Promise.allSettled(
newUsers.map(async (el) => {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: el.id,
orgId: dto.permission.orgId
});
if (el.email) {
signUpTokens.push({
email: el.email,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${dto.permission.orgId}`
});
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [el.email],
substitutions: {
inviterFirstName: actorDetails?.firstName,
inviterUsername: actorDetails?.email,
organizationName: orgDetails?.name,
email: el.email,
organizationId: orgDetails?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
}
})
);
return { signUpTokens };
};
const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => {

View File

@@ -11,19 +11,29 @@ import {
ProjectPermissionMemberActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { InternalServerError, PermissionBoundaryError } from "@app/lib/errors";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TMembershipUserDALFactory } from "../membership-user-dal";
import { TMembershipUserScopeFactory } from "../membership-user-types";
type TProjectMembershipUserScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRoles">;
orgDAL: Pick<TOrgDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "find">;
smtpService: Pick<TSmtpService, "sendMail">;
};
export const newProjectMembershipUserFactory = ({
permissionService,
orgDAL
orgDAL,
projectDAL,
membershipUserDAL,
smtpService
}: TProjectMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => {
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Project) {
@@ -41,7 +51,10 @@ export const newProjectMembershipUserFactory = ({
const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role);
const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => {
const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (
dto,
newUsers
) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
@@ -53,6 +66,21 @@ export const newProjectMembershipUserFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member);
// TODO(namespace): this becomes tricky in namespace due to group flow
const orgMemberships = await membershipUserDAL.find({
scope: AccessScope.Organization,
scopeOrgId: dto.permission.orgId,
$in: {
actorUserId: newUsers.map((el) => el.id)
}
});
if (orgMemberships.length !== newUsers.length) {
const missingUsers = newUsers
.filter((el) => !orgMemberships.find((memb) => memb.actorUserId === el.id))
.map((el) => el.email);
throw new BadRequestError({ message: `Users ${missingUsers.join(",")} not part of organization` });
}
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId);
const permissionRoles = await permissionService.getProjectPermissionByRoles(
dto.data.roles.filter((el) => el.role !== ProjectMembershipRole.NoAccess).map((el) => el.role),
@@ -80,8 +108,24 @@ export const newProjectMembershipUserFactory = ({
}
};
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => {
throw new InternalServerError({ message: "Project membership user create complete not implemented" });
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async (
dto,
newMembers
) => {
const appCfg = getConfig();
const scope = getScopeField(dto.scopeData);
const project = await projectDAL.findById(scope.value);
const emails = newMembers.filter((el) => Boolean(el?.email)).map((el) => el?.email as string);
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: emails,
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
return { signUpTokens: [] };
};
const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async (dto) => {

View File

@@ -344,7 +344,7 @@ export const orgDALFactory = (db: TDbClient) => {
const count = await db
.replicaNode()(TableName.Membership)
.where(`${TableName.Membership}.orgId`, orgId)
.where(`${TableName.Membership}.scopeOrgId`, orgId)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.whereNotNull(`${TableName.Membership}.actorUserId`)
.count("*")
@@ -376,7 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
conn.ref("id").withSchema(TableName.Membership),
conn.ref("inviteEmail").withSchema(TableName.Membership),
conn.ref("orgId").withSchema(TableName.Membership),
conn.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"),
db.ref("role").withSchema(TableName.MembershipRole),
db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"),
conn.ref("status").withSchema(TableName.Membership),
@@ -416,7 +416,7 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
conn.ref("id").withSchema(TableName.Membership),
conn.ref("inviteEmail").withSchema(TableName.Membership),
conn.ref("orgId").withSchema(TableName.Membership),
conn.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"),
conn.ref("role").withSchema(TableName.MembershipRole),
conn.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"),
conn.ref("status").withSchema(TableName.Membership),

View File

@@ -1,122 +1,59 @@
import { AccessScope } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { BadRequestError } from "@app/lib/errors";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
type TDeleteOrgMembership = {
orgMembershipId: string;
orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
userId?: string;
};
type TDeleteOrgMemberships = {
orgMembershipIds: string[];
orgId: string;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "deleteMembershipsById" | "transaction">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserIds">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete">;
orgDAL: Pick<TOrgDALFactory, "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "delete">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "delete" | "find">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
userId?: string;
};
export const deleteOrgMembershipFn = async ({
orgMembershipId,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
}: TDeleteOrgMembership) => {
const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx);
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
throw new BadRequestError({ message: "You cannot remove yourself from an organization" });
}
const deletedUserId = orgMembership.actorUserId;
if (!deletedUserId) {
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
await userAliasDAL.delete(
{
userId: deletedUserId,
orgId
},
tx
);
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, deletedUserId);
// Delete all the project memberships of the user in the organization
await membershipUserDAL.delete(
{
scope: AccessScope.Project,
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: deletedUserId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
});
return deletedMembership;
};
export const deleteOrgMembershipsFn = async ({
orgMembershipIds,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
membershipUserDAL,
userGroupMembershipDAL,
membershipRoleDAL
}: TDeleteOrgMemberships) => {
const deletedMemberships = await orgDAL.transaction(async (tx) => {
const orgMemberships = await orgDAL.deleteMembershipsById(orgMembershipIds, orgId, tx);
await membershipRoleDAL.delete(
{
$in: {
membershipId: orgMembershipIds
}
},
tx
);
const orgMemberships = await membershipUserDAL.delete(
{
scopeOrgId: orgId,
scope: AccessScope.Organization,
$in: {
id: orgMembershipIds
}
},
tx
);
const membershipUserIds = orgMemberships
.filter((member) => Boolean(member.actorUserId))
@@ -143,32 +80,44 @@ export const deleteOrgMembershipsFn = async ({
);
// Get all the project memberships of the users in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, membershipUserIds);
// Delete all the project memberships of the users in the organization
await membershipUserDAL.delete(
const otherMemberships = await membershipUserDAL.delete(
{
scope: AccessScope.Project,
scopeOrgId: orgId,
$in: {
id: projectMemberships.map((membership) => membership.id)
actorUserId: membershipUserIds
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId),
receiverId: membershipUserIds
}
const orgGroups = await membershipUserDAL.find({
scopeOrgId: orgId,
$notNull: ["actorGroupId"]
});
const groupIds = orgGroups.filter((el) => el.actorGroupId).map((el) => el.actorGroupId as string);
await userGroupMembershipDAL.delete(
{
$in: {
userId: membershipUserIds,
groupId: groupIds
}
},
tx
);
const projectIds = otherMemberships
.filter((el) => el.scope === AccessScope.Project && el.scopeProjectId)
.map((el) => el.scopeProjectId as string);
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
projectId: projectIds,
receiverId: membershipUserIds
}
},
tx

View File

@@ -1,237 +0,0 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
import {
orgAdminPermissions,
orgMemberPermissions,
orgNoAccessPermissions,
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
type TOrgRoleServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory;
permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
};
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({
orgRoleDAL,
orgDAL,
permissionService,
externalGroupOrgRoleMappingDAL
}: TOrgRoleServiceFactoryDep) => {
const createRole = async (
userId: string,
orgId: string,
data: Omit<TOrgRolesInsert, "orgId">,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
const role = await orgRoleDAL.create({
...data,
orgId,
permissions: JSON.stringify(data.permissions)
});
return role;
};
const getRole = async (
userId: string,
orgId: string,
roleId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
switch (roleId) {
case "b11b49a9-09a9-4443-916a-4246f9ff2c69": {
return {
id: roleId,
orgId,
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions),
createdAt: new Date(),
updatedAt: new Date()
};
}
case "b11b49a9-09a9-4443-916a-4246f9ff2c70": {
return {
id: roleId,
orgId,
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions),
createdAt: new Date(),
updatedAt: new Date()
};
}
case "b10d49a9-09a9-4443-916a-4246f9ff2c72": {
return {
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
orgId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions),
createdAt: new Date(),
updatedAt: new Date()
};
}
default: {
const role = await orgRoleDAL.findOne({ id: roleId, orgId });
if (!role) throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found` });
return role;
}
}
};
const updateRole = async (
userId: string,
orgId: string,
roleId: string,
data: Omit<TOrgRolesUpdate, "orgId">,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
if (data?.slug) {
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const [updatedRole] = await orgRoleDAL.update(
{ id: roleId, orgId },
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
);
if (!updatedRole) throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found` });
return updatedRole;
};
const deleteRole = async (
userId: string,
orgId: string,
roleId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const org = await orgDAL.findOrgById(orgId);
if (!org)
throw new NotFoundError({
message: `Organization with ID '${orgId}' not found`
});
if (org.defaultMembershipRole === roleId)
throw new BadRequestError({
message: "Cannot delete default org membership role. Please re-assign and try again."
});
const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({
orgId,
roleId
});
if (externalGroupMapping)
throw new BadRequestError({
message:
"Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again."
});
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deletedRole)
throw new NotFoundError({ message: `Organization role with ID '${roleId}' not found`, name: "UpdateRole" });
return deletedRole;
};
const listRoles = async (
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
const customRoles = await orgRoleDAL.find({ orgId });
const roles = [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
orgId,
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
orgId,
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
orgId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions),
createdAt: new Date(),
updatedAt: new Date()
},
...(customRoles || []).map(({ permissions, ...data }) => ({
...data,
permissions
}))
];
return roles;
};
const getUserPermission = async (
userId: string,
orgId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission, memberships } = await permissionService.getOrgPermission(
ActorType.USER,
userId,
orgId,
actorAuthMethod,
actorOrgId
);
return { permissions: packRules(permission.rules), memberships };
};
return { createRole, getRole, updateRole, deleteRole, listRoles, getUserPermission };
};

View File

@@ -4,19 +4,14 @@ import { Knex } from "knex";
import {
AccessScope,
ActionProjectType,
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
TableName,
TOidcConfigs,
TProjectMemberships,
TProjectUserMembershipRolesInsert,
TSamlConfigs,
TUsers
TSamlConfigs
} from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLdapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
@@ -26,29 +21,16 @@ import {
OrgPermissionSecretShareAction,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import {
BadRequestError,
ForbiddenRequestError,
NotFoundError,
PermissionBoundaryError,
UnauthorizedError
} from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { QueueName } from "@app/queue";
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
@@ -76,7 +58,7 @@ import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
import { TOrgBotDALFactory } from "./org-bot-dal";
import { TOrgDALFactory } from "./org-dal";
import { deleteOrgMembershipFn, deleteOrgMembershipsFn } from "./org-fns";
import { deleteOrgMembershipsFn } from "./org-fns";
import {
TDeleteOrgMembershipDTO,
TDeleteOrgMembershipsDTO,
@@ -84,7 +66,6 @@ import {
TFindOrgMembersByEmailDTO,
TGetOrgGroupsDTO,
TGetOrgMembershipDTO,
TInviteUserToOrgDTO,
TListProjectMembershipsByOrgMembershipIdDTO,
TResendOrgMemberInvitationDTO,
TUpdateOrgDTO,
@@ -130,6 +111,7 @@ type TOrgServiceFactoryDep = {
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
};
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -161,7 +143,8 @@ export const orgServiceFactory = ({
loginService,
reminderService,
membershipRoleDAL,
membershipUserDAL
membershipUserDAL,
userGroupMembershipDAL
}: TOrgServiceFactoryDep) => {
/*
* Get organization details by the organization id
@@ -898,369 +881,6 @@ export const orgServiceFactory = ({
return { signupToken: undefined };
};
/*
* Invite user to organization
*/
const inviteUserToOrganization = async ({
orgId,
actorId,
actor,
inviteeEmails,
organizationRoleSlug,
projects: invitedProjects,
actorAuthMethod,
actorOrgId
}: TInviteUserToOrgDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const invitingUser = await userDAL.findOne({ id: actorId });
const org = await orgDAL.findOrgById(orgId);
const isEmailInvalid = await isDisposableEmail(inviteeEmails);
if (isEmailInvalid) {
throw new BadRequestError({
message: "Disposable emails are not allowed",
name: "InviteUser"
});
}
const plan = await licenseService.getPlan(orgId);
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
if (isCustomOrgRole) {
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
}
const projectsToInvite = invitedProjects?.length
? await projectDAL.find({
orgId,
$in: {
id: invitedProjects?.map(({ id }) => id)
}
})
: [];
if (projectsToInvite.length !== invitedProjects?.length) {
throw new ForbiddenRequestError({
message: "Access denied to one or more of the specified projects"
});
}
if (projectsToInvite.some((el) => el.version !== ProjectVersion.V3)) {
throw new BadRequestError({
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
});
}
const mailsForOrgInvitation: { email: string; userId: string; firstName: string; lastName: string }[] = [];
const mailsForProjectInvitation: { email: string[]; projectName: string }[] = [];
const newProjectMemberships: TProjectMemberships[] = [];
await orgDAL.transaction(async (tx) => {
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
for await (const inviteeEmail of inviteeEmails) {
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
);
}
}
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
);
}
const [inviteeOrgMembership] = await orgDAL.findMembership(
{
[`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId,
[`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization,
[`${TableName.Membership}.id` as "id"]: inviteeUserId
},
{ tx }
);
// if there exist no org membership we set is as given by the request
if (!inviteeOrgMembership) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({
name: "InviteUser",
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
if (org?.authEnforced) {
throw new ForbiddenRequestError({
name: "InviteUser",
message: "Failed to invite user due to org-level auth enforced for organization"
});
}
// as its used by project invite also
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
let roleId;
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
if (isCustomOrgRole) {
const customRole = await roleDAL.findOne({ slug: organizationRoleSlug, orgId });
if (!customRole) {
throw new NotFoundError({
name: "InviteUser",
message: `Custom organization role with slug '${orgRole}' not found`
});
}
roleId = customRole.id;
}
const membership = await orgDAL.createMembership(
{
actorUserId: inviteeUser.id,
inviteEmail: inviteeEmail,
scopeOrgId: orgId,
status: OrgMembershipStatus.Invited,
isActive: true,
scope: AccessScope.Organization
},
tx
);
await membershipRoleDAL.create(
{
membershipId: membership.id,
role: orgRole,
customRoleId: roleId
},
tx
);
mailsForOrgInvitation.push({
email: inviteeEmail,
userId: inviteeUser.id,
firstName: inviteeUser?.firstName || "",
lastName: inviteeUser.lastName || ""
});
}
users.push(inviteeUser);
}
const userIds = users.map(({ id }) => id);
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
// we don't need to spam with email. Thus org invitation doesn't need project invitation again
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
// if there exist no project membership we set is as given by the request
for await (const project of projectsToInvite) {
const projectId = project.id;
const { permission: projectPermission, membership } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionMemberActions.Create,
ProjectPermissionSub.Member
);
const existingMembers = await membershipUserDAL.find(
{
scopeOrgId: project.orgId,
scope: AccessScope.Project,
scopeProjectId: project.id,
$in: { userId: userIds }
},
{ tx }
);
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.actorUserId as string);
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
(user) => !existingMembersGroupByUserId?.[user.userId]
);
// eslint-disable-next-line no-continue
if (!userWithEncryptionKeyInvitedToProject.length) continue;
// validate custom project role
const invitedProjectRoles = invitedProjects.find((el) => el.id === project.id)?.projectRoleSlug || [
ProjectMembershipRole.Member
];
for await (const invitedRole of invitedProjectRoles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
invitedRole,
projectId
);
if (invitedRole !== ProjectMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
projectPermission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to invite user to the project",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
const customProjectRoles = invitedProjectRoles.filter(
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customProjectRoles.length);
if (hasCustomRole) {
if (!plan?.rbac)
throw new BadRequestError({
name: "InviteUser",
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
}
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customProjectRoles.map((role) => role) }
})
: [];
if (customRoles.length !== customProjectRoles.length) {
throw new NotFoundError({ name: "InviteUser", message: "Custom project role not found" });
}
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const projectMemberships = await projectMembershipDAL.insertMany(
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
projectId,
userId: userEnc.userId
})),
tx
);
newProjectMemberships.push(...projectMemberships);
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
invitedProjectRoles.forEach((projectRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
projectMemberships.forEach((membershipEntry) => {
sanitizedProjectMembershipRoles.push({
projectMembershipId: membershipEntry.id,
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
});
});
});
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
mailsForProjectInvitation.push({
email: userWithEncryptionKeyInvitedToProject
.filter((el) => !userIdsWithOrgInvitation.has(el.userId))
.map((el) => el.email || el.username),
projectName: project.name
});
}
return users;
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);
const signupTokens: { email: string; link: string }[] = [];
// send org invite mail
await Promise.allSettled(
mailsForOrgInvitation.map(async (el) => {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: el.userId,
orgId
});
signupTokens.push({
email: el.email,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${el.email}&organization_id=${org?.id}`
});
return smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [el.email],
substitutions: {
inviterFirstName: invitingUser?.firstName,
inviterUsername: invitingUser?.email,
organizationName: org?.name,
email: el.email,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
})
);
await Promise.allSettled(
mailsForProjectInvitation
.filter((el) => Boolean(el.email.length))
.map(async (el) => {
return smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: el.email,
substitutions: {
workspaceName: el.projectName,
callback_url: `${appCfg.SITE_URL}/login`
}
});
})
);
if (!appCfg.isSmtpConfigured) {
return { signupTokens, projectMemberships: newProjectMemberships };
}
return { signupTokens: undefined, projectMemberships: newProjectMemberships };
};
/**
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
* magic link and issue a temporary signup token for user to complete setting up their account
@@ -1370,16 +990,17 @@ export const orgServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const deletedMembership = await deleteOrgMembershipFn({
orgMembershipId: membershipId,
const [deletedMembership] = await deleteOrgMembershipsFn({
orgMembershipIds: [membershipId],
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
membershipUserDAL,
membershipRoleDAL,
userGroupMembershipDAL
});
return deletedMembership;
@@ -1409,12 +1030,13 @@ export const orgServiceFactory = ({
orgMembershipIds: membershipIds,
orgId,
orgDAL,
projectMembershipDAL,
projectKeyDAL,
userAliasDAL,
licenseService,
userId,
membershipUserDAL
membershipUserDAL,
membershipRoleDAL,
userGroupMembershipDAL
});
return deletedMemberships;
@@ -1572,7 +1194,6 @@ export const orgServiceFactory = ({
findAllOrgMembers,
findAllOrganizationOfUser,
findIdentityOrganization,
inviteUserToOrganization,
verifyUserToOrg,
updateOrg,
findOrgMembersByUsername,

View File

@@ -24,7 +24,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
.join<TMemberships>(db(TableName.Membership).as("orgMembership"), (qb) => {
qb.on(`${TableName.Users}.id`, "=", `orgMembership.actorUserId`)
.andOn(`orgMembership.scopeOrgId`, "=", `${TableName.Project}.orgId`)
.andOn("orgMembership.scope", AccessScope.Organization);
.andOn("orgMembership.scope", db.raw("?", [AccessScope.Organization]));
})
.where((qb) => {
if (filter.usernames) {

View File

@@ -3,59 +3,42 @@ import { ForbiddenError } from "@casl/ability";
import { AccessScope, ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal";
import { ActorType } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
import { TNotificationServiceFactory } from "../notification/notification-service";
import { NotificationType } from "../notification/notification-types";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import {
ProjectUserMembershipTemporaryMode,
TAddUsersToWorkspaceDTO,
TDeleteProjectMembershipOldDTO,
TDeleteProjectMembershipsDTO,
TGetProjectMembershipByIdDTO,
TGetProjectMembershipByUsernameDTO,
TGetProjectMembershipDTO,
TLeaveProjectDTO,
TUpdateProjectMembershipDTO
TLeaveProjectDTO
} from "./project-membership-types";
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
import { TRoleDALFactory } from "../role/role-dal";
import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal";
import { TMembershipDALFactory } from "../membership/membership-dal";
import { TMembershipRoleDALFactory } from "../membership/membership-role-dal";
type TProjectMembershipServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRoles"| "invalidateProjectPermissionCache">;
smtpService: TSmtpService;
projectMembershipDAL: TProjectMembershipDALFactory;
membershipDAL: TMembershipDALFactory;
membershipUserDAL: TMembershipUserDALFactory;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
userDAL: Pick<TUserDALFactory, "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
roleDAL: Pick<TRoleDALFactory, "find" | "findOne">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -70,21 +53,17 @@ export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembersh
export const projectMembershipServiceFactory = ({
permissionService,
projectMembershipDAL,
projectUserMembershipRoleDAL,
smtpService,
orgDAL,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectDAL,
projectKeyDAL,
secretReminderRecipientsDAL,
licenseService,
notificationService,
roleDAL,
membershipRoleDAL,
additionalPrivilegeDAL,
membershipDAL
membershipUserDAL,
userDAL,
membershipRoleDAL
}: TProjectMembershipServiceFactoryDep) => {
const getProjectMemberships = async ({
actorId,
@@ -153,30 +132,6 @@ export const projectMembershipServiceFactory = ({
return membership;
};
const getProjectMembershipById = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
id
}: TGetProjectMembershipByIdDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id });
if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` });
return membership;
};
// TODO(simp): cross-check akhil
const addUsersToProject = async ({
projectId,
actorId,
@@ -198,7 +153,7 @@ export const projectMembershipServiceFactory = ({
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member);
const orgMembers = await membershipDAL.find({
const orgMembers = await membershipUserDAL.find({
[`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: project.orgId,
scope: AccessScope.Organization,
$in: {
@@ -208,41 +163,48 @@ export const projectMembershipServiceFactory = ({
if (orgMembers.length !== members.length) throw new BadRequestError({ message: "Some users are not part of org" });
const existingMembers = await membershipDAL.find({
const existingMembers = await membershipUserDAL.find({
[`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId,
scope: AccessScope.Project,
$in: { actorUserId: orgMembers.map(({ actorUserId }) => actorUserId).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const orgMembershipUsernames = await userDAL.find({
$in: {
id: orgMembers.filter((el) => Boolean(el.actorUserId)).map((el) => el.actorUserId as string)
}
});
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(
orgMembers.map(({ username }) => username),
orgMembershipUsernames.map(({ username }) => username),
projectId
)
);
await membershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
userId
await membershipUserDAL.transaction(async (tx) => {
const projectMemberships = await membershipUserDAL.insertMany(
orgMembers.map(({ actorUserId }) => ({
scopeProjectId: projectId,
actorUserId,
scope: AccessScope.Project,
scopeOrgId: project.orgId
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
await membershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ membershipId: id, role: ProjectMembershipRole.Member })),
tx
);
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId))
.map(({ userId, id }) => ({
.filter(({ actorUserId }) => !userIdsToExcludeForProjectKeyAddition.has(actorUserId as string))
.map(({ actorUserId, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: actorId,
receiverId: userId,
receiverId: actorUserId as string,
projectId
})),
tx
@@ -253,8 +215,8 @@ export const projectMembershipServiceFactory = ({
if (sendEmails) {
await notificationService.createUserNotifications(
orgMembers.map((member) => ({
userId: member.userId,
orgMembershipUsernames.map((member) => ({
userId: member.id,
orgId: project.orgId,
type: NotificationType.PROJECT_INVITATION,
title: "Project Invitation",
@@ -266,7 +228,7 @@ export const projectMembershipServiceFactory = ({
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: orgMembers.filter((i) => i.email).map((i) => i.email as string),
recipients: orgMembershipUsernames.filter((i) => i.email).map((i) => i.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
@@ -276,164 +238,6 @@ export const projectMembershipServiceFactory = ({
return orgMembers;
};
const updateProjectMembership = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
membershipId,
roles
}: TUpdateProjectMembershipDTO) => {
const { permission, memberships } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) {
throw new ForbiddenRequestError({ message: "Forbidden member update" });
}
const providedRolePermissionDetails = await permissionService.getProjectPermissionByRoles(
roles.map((el) => el.role).filter((el) => el !== ProjectMembershipRole.NoAccess),
projectId
);
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId);
for await (const { permission: rolePermission } of providedRolePermissionDetails) {
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to assign to role",
shouldUseNewPrivilegeSystem,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionSub.Member
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) =>
!Object.values(ProjectMembershipRole)
// we don't want to include custom in this check;
// this unintentionally enables setting slug to custom which is reserved
.filter((r) => r !== ProjectMembershipRole.Custom)
.includes(role as ProjectMembershipRole.Member)
);
const hasCustomRole = Boolean(customInputRoles.length);
if (hasCustomRole) {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
}
const customRoles = hasCustomRole
? await roleDAL.find({
projectId,
$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 sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: membershipId,
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: membershipId,
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 membershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: membershipId }, tx);
return projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
});
await permissionService.invalidateProjectPermissionCache(projectId);
return updatedRoles;
};
// This is old and should be removed later. Its not used anywhere, but it is exposed in our API. So to avoid breaking changes, we are keeping it for now.
const deleteProjectMembership = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
membershipId
}: TDeleteProjectMembershipOldDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
const member = await userDAL.findUserByProjectMembershipId(membershipId);
if (member?.isGhost) {
throw new ForbiddenRequestError({
message: "Forbidden membership deletion",
name: "DeleteProjectMembership"
});
}
const membership = await projectMembershipDAL.transaction(async (tx) => {
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
await secretReminderRecipientsDAL.delete(
{
projectId,
userId: deletedMembership.userId
},
tx
);
return deletedMembership;
});
await permissionService.invalidateProjectPermissionCache(projectId);
return membership;
};
const deleteProjectMemberships = async ({
actorId,
actor,
@@ -485,20 +289,21 @@ export const projectMembershipServiceFactory = ({
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
const memberships = await projectMembershipDAL.transaction(async (tx) => {
await projectUserAdditionalPrivilegeDAL.delete(
const memberships = await membershipUserDAL.transaction(async (tx) => {
await additionalPrivilegeDAL.delete(
{
projectId,
$in: {
userId: projectMembers.map((membership) => membership.user.id)
actorUserId: projectMembers.map((membership) => membership.user.id)
}
},
tx
);
const deletedMemberships = await projectMembershipDAL.delete(
const deletedMemberships = await membershipUserDAL.delete(
{
projectId,
scopeProjectId: projectId,
scope: AccessScope.Project,
$in: {
id: projectMembers.map(({ id }) => id)
}
@@ -571,11 +376,11 @@ export const projectMembershipServiceFactory = ({
});
}
const deletedMembership = await projectMembershipDAL.transaction(async (tx) => {
await projectUserAdditionalPrivilegeDAL.delete(
const deletedMembership = await membershipUserDAL.transaction(async (tx) => {
await additionalPrivilegeDAL.delete(
{
projectId: project.id,
userId: actorId
actorUserId: actorId
},
tx
);
@@ -591,10 +396,11 @@ export const projectMembershipServiceFactory = ({
);
const membership = (
await projectMembershipDAL.delete(
await membershipUserDAL.delete(
{
projectId: project.id,
userId: actorId
scope: AccessScope.Project,
scopeProjectId: project.id,
actorUserId: actorId
},
tx
)
@@ -612,11 +418,8 @@ export const projectMembershipServiceFactory = ({
return {
getProjectMemberships,
getProjectMembershipByUsername,
updateProjectMembership,
deleteProjectMemberships,
deleteProjectMembership, // TODO: Remove this
addUsersToProject,
leaveProject,
getProjectMembershipById
leaveProject
};
};

View File

@@ -1,285 +0,0 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionActions,
ProjectPermissionSet,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import {
ProjectRoleServiceIdentifierType,
TCreateRoleDTO,
TDeleteRoleDTO,
TGetRoleDetailsDTO,
TListRolesDTO,
TUpdateRoleDTO
} from "./project-role-types";
type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
identityDAL: Pick<TIdentityDALFactory, "findById">;
userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findProjectById">;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserProjectPermission" | "invalidateProjectPermissionCache"
>;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
};
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const projectRoleServiceFactory = ({
projectRoleDAL,
permissionService,
identityProjectMembershipRoleDAL,
projectUserMembershipRoleDAL,
projectDAL,
identityDAL,
userDAL
}: TProjectRoleServiceFactoryDep) => {
const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => {
let projectId = "";
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: "Project not found" });
projectId = project.id;
} else {
projectId = filter.projectId;
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
if (existingRole) {
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
}
validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const role = await projectRoleDAL.create({
...data,
projectId
});
return { ...role, permissions: unpackPermissions(role.permissions) };
};
const getRoleBySlug = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
roleSlug,
filter
}: TGetRoleDetailsDTO) => {
let project: TProjects;
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
} else {
project = await projectDAL.findProjectById(filter.projectId);
}
if (!project) throw new NotFoundError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) {
const [predefinedRole] = getPredefinedRoles({
projectId: project.id,
projectType: project.type as ProjectType,
roleFilter: roleSlug as ProjectMembershipRole
});
if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` });
return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) };
}
const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId: project.id });
if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` });
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
};
const updateRole = async ({ roleId, actorOrgId, actorAuthMethod, actorId, actor, data }: TUpdateRoleDTO) => {
const projectRole = await projectRoleDAL.findById(roleId);
if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectRole.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
if (data?.slug) {
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId: projectRole.projectId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
}
validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
...data,
permissions: data.permissions ? data.permissions : undefined
});
if (!updatedRole) throw new NotFoundError({ message: "Project role not found", name: "Update role" });
await permissionService.invalidateProjectPermissionCache(projectRole.projectId);
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
};
const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, roleId }: TDeleteRoleDTO) => {
const projectRole = await projectRoleDAL.findById(roleId);
if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectRole.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId });
const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId });
if (identityRole) {
throw new BadRequestError({
message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.",
name: "Delete role"
});
}
if (projectUserRole) {
throw new BadRequestError({
message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.",
name: "Delete role"
});
}
const deletedRole = await projectRoleDAL.deleteById(roleId);
if (!deletedRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
await permissionService.invalidateProjectPermissionCache(projectRole.projectId);
return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) };
};
const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => {
let project: TProjects;
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
} else {
project = await projectDAL.findProjectById(filter.projectId);
}
if (!project) throw new BadRequestError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
const customRoles = await projectRoleDAL.find(
{ projectId: project.id },
{ sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] }
);
const roles = [
...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }),
...(customRoles || [])
];
return roles;
};
const getUserPermission = async (
userId: string,
projectId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission, memberships } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: userId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// just to satisfy ts
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === projectId;
const assumedPrivilegeDetails = isAssumingPrivilege
? {
actorId: assumedPrivilegeDetailsCtx?.actorId,
actorType: assumedPrivilegeDetailsCtx?.actorType,
actorName: "",
actorEmail: ""
}
: undefined;
if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) {
const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId);
if (!identityDetails)
throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = identityDetails.name;
} else if (assumedPrivilegeDetails?.actorType === ActorType.USER) {
const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId);
if (!userDetails)
throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`;
assumedPrivilegeDetails.actorEmail = userDetails?.email || "";
}
return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
};

View File

@@ -167,6 +167,13 @@ export const roleServiceFactory = ({
await factory.onGetRoleByIdGuard(dto);
const scope = factory.getScopeField(scopeData);
const predefinedRole = await factory.getPredefinedRoles(scopeData);
const selectedRole = predefinedRole.find((el) => el.id === dto.selector.id);
if (selectedRole) {
return { ...selectedRole, permissions: UnpackedPermissionSchema.array().parse(selectedRole.permissions) };
}
const role = await roleDAL.findOne({
id: selector.id,
[scope.key]: scope.value

View File

@@ -76,7 +76,7 @@ type TSuperAdminServiceFactoryDep = {
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
kmsRootConfigDAL: TKmsRootConfigDALFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "inviteUserToOrganization">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "updateSubscriptionOrgMemberCount">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;

View File

@@ -39,7 +39,7 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) =>
});
return data;
},
enabled: Boolean(dto.organizationId)
enabled: Boolean(dto.organizationId && !dto.isDisabled)
});
};

View File

@@ -44,6 +44,7 @@ export interface GetIdentityAuthTemplatesDTO {
limit?: number;
offset?: number;
search?: string;
isDisabled?: boolean;
}
export interface MachineAuthTemplateUsage {

View File

@@ -1,4 +1,3 @@
import { packRules } from "@casl/ability/extra";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
@@ -71,14 +70,10 @@ export const useCreateOrgRole = () => {
const queryClient = useQueryClient();
return useMutation<TOrgRole, object, TCreateOrgRoleDTO>({
mutationFn: async ({ orgId, permissions, ...dto }: TCreateOrgRoleDTO) => {
mutationFn: async ({ orgId, ...dto }: TCreateOrgRoleDTO) => {
const {
data: { role }
} = await apiRequest.post(`/api/v1/organization/${orgId}/roles`, {
...dto,
// TODO(simp): removing packing
permissions: permissions.length ? packRules(permissions) : []
});
} = await apiRequest.post(`/api/v1/organization/${orgId}/roles`, dto);
return role;
},
@@ -92,13 +87,10 @@ export const useUpdateOrgRole = () => {
const queryClient = useQueryClient();
return useMutation<TOrgRole, object, TUpdateOrgRoleDTO>({
mutationFn: async ({ id, orgId, permissions, ...dto }: TUpdateOrgRoleDTO) => {
mutationFn: async ({ id, orgId, ...dto }: TUpdateOrgRoleDTO) => {
const {
data: { role }
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, {
...dto,
permissions: permissions ? packRules(permissions) : undefined
});
} = await apiRequest.patch(`/api/v1/organization/${orgId}/roles/${id}`, dto);
return role;
},

View File

@@ -80,10 +80,7 @@ const getOrgRoles = async (orgId: string) => {
const { data } = await apiRequest.get<{
data: { roles: Array<Omit<TOrgRole, "permissions"> & { permissions: unknown }> };
}>(`/api/v1/organization/${orgId}/roles`);
return data.data.roles.map(({ permissions, ...el }) => ({
...el,
permissions: unpackRules(permissions as PackRule<TPermission>[])
}));
return data.data.roles;
};
export const useGetOrgRoles = (orgId: string, enable = true) =>
@@ -102,7 +99,7 @@ export const useGetOrgRole = (orgId: string, roleId: string) =>
}>(`/api/v1/organization/${orgId}/roles/${roleId}`);
return {
...data.role,
permissions: unpackRules(data.role.permissions as PackRule<TPermission>[])
permissions: data.role.permissions as PackRule<TPermission>[]
};
},
enabled: Boolean(orgId && roleId)

View File

@@ -1,6 +1,7 @@
import {
faArrowDown,
faArrowUp,
faBan,
faEdit,
faEllipsisV,
faEye,
@@ -30,7 +31,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionSubjects, useOrganization, useSubscription } from "@app/context";
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
import {
getUserTablePreference,
@@ -86,12 +87,14 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
};
const organizationId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { data, isPending, isFetching } = useGetIdentityAuthTemplates({
organizationId,
limit,
offset,
search: debouncedSearch
search: debouncedSearch,
isDisabled: !subscription.machineIdentityAuthTemplates
});
const { templates = [], totalCount = 0 } = data ?? {};
@@ -172,7 +175,10 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="identity-auth-templates" />}
{subscription.machineIdentityAuthTemplates && isPending && (
<TableSkeleton columns={4} innerKey="identity-auth-templates" />
)}
{!isPending &&
templates?.map((template) => (
<Tr
@@ -265,6 +271,9 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
onChangePerPage={handlePerPageChange}
/>
)}
{!subscription.machineIdentityAuthTemplates && (
<EmptyState title="This feature is not yet activated for your license." icon={faBan} />
)}
{!isPending && templates.length === 0 && (
<EmptyState
title={