diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 4e1f7d1f8..58daca232 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -58,7 +58,6 @@ type TSecretApprovalRequestServiceFactoryDep = { "create" | "find" | "findOne" | "transaction" | "delete" >; groupDAL: Pick; - projectMembershipDAL: Pick; smtpService: Pick; userDAL: Pick< TUserDALFactory, diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 0ec9e646e..32e5f18be 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -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; - projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, | "create" @@ -78,7 +76,7 @@ type TScimServiceFactoryDep = { | "update" >; membershipGroupDAL: Pick; - membershipRoleDAL: Pick; + 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 diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3ca90a54b..1b3301500 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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({ diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index b98e94be0..0f78e0448 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -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({ diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index bcd24295d..33e143d2a 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -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 + } + }; } }); }; diff --git a/backend/src/server/routes/v2/deprecated-identity-project-router.ts b/backend/src/server/routes/v2/deprecated-identity-project-router.ts index c16c874ae..85ea7d112 100644 --- a/backend/src/server/routes/v2/deprecated-identity-project-router.ts +++ b/backend/src/server/routes/v2/deprecated-identity-project-router.ts @@ -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 } + }; } }); diff --git a/backend/src/server/routes/v2/deprecated-project-membership-router.ts b/backend/src/server/routes/v2/deprecated-project-membership-router.ts index d88d2f996..4dff4d5ea 100644 --- a/backend/src/server/routes/v2/deprecated-project-membership-router.ts +++ b/backend/src/server/routes/v2/deprecated-project-membership-router.ts @@ -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 + })) + }; } }); }; diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 9135d2356..1d9ebe8b3 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -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 + })) + }; } }); diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 7cd606032..956008b12 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -193,62 +193,62 @@ export const identityOrgDALFactory = (db: TDbClient) => { }) .leftJoin( TableName.IdentityUniversalAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityTokenAuth}.identityId` ) .leftJoin( TableName.IdentityJwtAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityJwtAuth}.identityId` ) .leftJoin( TableName.IdentityLdapAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityLdapAuth}.identityId` ) .leftJoin( TableName.IdentityTlsCertAuth, - "paginatedIdentity.identityId", + "paginatedIdentity.actorIdentityId", `${TableName.IdentityTlsCertAuth}.identityId` ) .select( diff --git a/backend/src/services/membership-group/membership-group-service.ts b/backend/src/services/membership-group/membership-group-service.ts index 6a396e4e5..0081bc4ad 100644 --- a/backend/src/services/membership-group/membership-group-service.ts +++ b/backend/src/services/membership-group/membership-group-service.ts @@ -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; roleDAL: Pick; + permissionService: TPermissionServiceFactory; + orgDAL: TOrgDALFactory; }; export type TMembershipGroupServiceFactory = ReturnType; @@ -27,9 +33,22 @@ export type TMembershipGroupServiceFactory = ReturnType { - const scopeFactory: Record = {}; + 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; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index c3d741a4d..be90045ee 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -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; - licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; roleDAL: Pick; - 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; @@ -50,20 +54,35 @@ export type TMembershipUserServiceFactory = ReturnType { 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; diff --git a/backend/src/services/membership-user/membership-user-types.ts b/backend/src/services/membership-user/membership-user-types.ts index c9c567b44..b8761671c 100644 --- a/backend/src/services/membership-user/membership-user-types.ts +++ b/backend/src/services/membership-user/membership-user-types.ts @@ -2,11 +2,18 @@ import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; import { OrgServiceActor } from "@app/lib/types"; export interface TMembershipUserScopeFactory { - onCreateMembershipUserGuard: (arg: TCreateMembershipUserDTO) => Promise; - onCreateMembershipComplete: (arg: { id: string; email: string }[]) => Promise; + onCreateMembershipUserGuard: ( + arg: TCreateMembershipUserDTO, + newMembers: { id: string; email?: string | null }[] + ) => Promise; + onCreateMembershipComplete: ( + arg: TCreateMembershipUserDTO, + newMembers: { id: string; email?: string | null }[] + ) => Promise<{ signUpTokens: { email: string; link: string }[] }>; onUpdateMembershipUserGuard: (arg: TUpdateMembershipUserDTO) => Promise; - onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO) => Promise; + onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO | TBulkDeleteMembershipByUsernameDTO) => Promise; + onListMembershipUserGuard: (arg: TListMembershipUserDTO) => Promise; onGetMembershipUserByUserIdGuard: (arg: TGetMembershipUserByUserIdDTO) => Promise; 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; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 8465d8f57..34f6784ff 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -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; + tokenService: Pick; + userDAL: Pick; + smtpService: Pick; + orgDAL: Pick; + userGroupMembershipDAL: Pick; + licenseService: Pick; }; 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) => { diff --git a/backend/src/services/membership-user/project/project-membership-user-factory.ts b/backend/src/services/membership-user/project/project-membership-user-factory.ts index bc860d2ad..6957f565f 100644 --- a/backend/src/services/membership-user/project/project-membership-user-factory.ts +++ b/backend/src/services/membership-user/project/project-membership-user-factory.ts @@ -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; orgDAL: Pick; + projectDAL: Pick; + membershipUserDAL: Pick; + smtpService: Pick; }; 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) => { diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d6367dfda..7a2d98c13 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -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), diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index fd998d43b..d1a378c33 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -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; - projectMembershipDAL: Pick; - membershipUserDAL: Pick; - projectKeyDAL: Pick; - userAliasDAL: Pick; - licenseService: Pick; - userId?: string; -}; - type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; - orgDAL: Pick; - projectMembershipDAL: Pick; - membershipUserDAL: Pick; + orgDAL: Pick; + userGroupMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipRoleDAL: Pick; projectKeyDAL: Pick; userAliasDAL: Pick; licenseService: Pick; 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 diff --git a/backend/src/services/org/org-role-service.ts b/backend/src/services/org/org-role-service.ts deleted file mode 100644 index 4afb6f791..000000000 --- a/backend/src/services/org/org-role-service.ts +++ /dev/null @@ -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; - -export const orgRoleServiceFactory = ({ - orgRoleDAL, - orgDAL, - permissionService, - externalGroupOrgRoleMappingDAL -}: TOrgRoleServiceFactoryDep) => { - const createRole = async ( - userId: string, - orgId: string, - data: Omit, - 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, - 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 }; -}; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 74729326b..3dd443cb8 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -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; loginService: Pick; reminderService: Pick; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; }; export type TOrgServiceFactory = ReturnType; @@ -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[] = []; - - 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, diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index d023e8f3f..6b1f192e7 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -24,7 +24,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { .join(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) { diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index c6118a044..a2cbcc43e 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -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; smtpService: TSmtpService; projectMembershipDAL: TProjectMembershipDALFactory; - membershipDAL: TMembershipDALFactory; + membershipUserDAL: TMembershipUserDALFactory; membershipRoleDAL: Pick; - userDAL: Pick; + userDAL: Pick; userGroupMembershipDAL: TUserGroupMembershipDALFactory; - roleDAL: Pick; - orgDAL: Pick; projectDAL: Pick; projectKeyDAL: Pick; licenseService: Pick; @@ -70,21 +53,17 @@ export type TProjectMembershipServiceFactory = ReturnType { 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 }; }; diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts deleted file mode 100644 index b4bfaac39..000000000 --- a/backend/src/services/project-role/project-role-service.ts +++ /dev/null @@ -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; - userDAL: Pick; - projectDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getUserProjectPermission" | "invalidateProjectPermissionCache" - >; - identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory; - projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory; -}; - -export type TProjectRoleServiceFactory = ReturnType; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -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 }; -}; diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts index 8a1c9c5b1..741b7ec9c 100644 --- a/backend/src/services/role/role-service.ts +++ b/backend/src/services/role/role-service.ts @@ -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 diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 72642f43e..c7ba673eb 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -76,7 +76,7 @@ type TSuperAdminServiceFactoryDep = { authService: Pick; kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; - orgService: Pick; + orgService: Pick; keyStore: Pick; licenseService: Pick; microsoftTeamsService: Pick; diff --git a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx index a9981c0c4..90c4c6c3c 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx +++ b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx @@ -39,7 +39,7 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => }); return data; }, - enabled: Boolean(dto.organizationId) + enabled: Boolean(dto.organizationId && !dto.isDisabled) }); }; diff --git a/frontend/src/hooks/api/identityAuthTemplates/types.ts b/frontend/src/hooks/api/identityAuthTemplates/types.ts index 860f9a9fc..d446afe52 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/types.ts +++ b/frontend/src/hooks/api/identityAuthTemplates/types.ts @@ -44,6 +44,7 @@ export interface GetIdentityAuthTemplatesDTO { limit?: number; offset?: number; search?: string; + isDisabled?: boolean; } export interface MachineAuthTemplateUsage { diff --git a/frontend/src/hooks/api/roles/mutation.tsx b/frontend/src/hooks/api/roles/mutation.tsx index f62f2a7c0..83214f4dc 100644 --- a/frontend/src/hooks/api/roles/mutation.tsx +++ b/frontend/src/hooks/api/roles/mutation.tsx @@ -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({ - 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({ - 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; }, diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index c52b2e3c1..4d4feef99 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -80,10 +80,7 @@ const getOrgRoles = async (orgId: string) => { const { data } = await apiRequest.get<{ data: { roles: Array & { permissions: unknown }> }; }>(`/api/v1/organization/${orgId}/roles`); - return data.data.roles.map(({ permissions, ...el }) => ({ - ...el, - permissions: unpackRules(permissions as PackRule[]) - })); + 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[]) + permissions: data.role.permissions as PackRule[] }; }, enabled: Boolean(orgId && roleId) diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx index 406f124dc..8733b3852 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx @@ -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) => { - {isPending && } + {subscription.machineIdentityAuthTemplates && isPending && ( + + )} + {!isPending && templates?.map((template) => ( { onChangePerPage={handlePerPageChange} /> )} + {!subscription.machineIdentityAuthTemplates && ( + + )} {!isPending && templates.length === 0 && (