diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 088d99ee9..e3e8733f0 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -20,8 +20,6 @@ import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2 import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template"; -import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; -import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service"; @@ -35,7 +33,6 @@ import { TPamSessionServiceFactory } from "@app/ee/services/pam-session/pam-sess import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types"; -import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types"; import { TRelayServiceFactory } from "@app/ee/services/relay/relay-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types"; @@ -53,6 +50,7 @@ import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-servi import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; +import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; @@ -65,6 +63,7 @@ import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-a import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; +import { TConvertorServiceFactory } from "@app/services/convertor/convertor-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service"; @@ -88,10 +87,12 @@ import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-a import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; +import { TMembershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; +import { TMembershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; +import { TMembershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; import { TOfflineUsageReportServiceFactory } from "@app/services/offline-usage-report/offline-usage-report-service"; -import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; @@ -104,8 +105,8 @@ import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; -import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { TReminderServiceFactory } from "@app/services/reminder/reminder-types"; +import { TRoleServiceFactory } from "@app/services/role/role-service"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; @@ -213,7 +214,6 @@ declare module "fastify" { authToken: TAuthTokenServiceFactory; permission: TPermissionServiceFactory; org: TOrgServiceFactory; - orgRole: TOrgRoleServiceFactory; oidc: TOidcConfigServiceFactory; superAdmin: TSuperAdminServiceFactory; user: TUserServiceFactory; @@ -225,7 +225,6 @@ declare module "fastify" { projectMembership: TProjectMembershipServiceFactory; projectEnv: TProjectEnvServiceFactory; projectKey: TProjectKeyServiceFactory; - projectRole: TProjectRoleServiceFactory; secret: TSecretServiceFactory; secretReplication: TSecretReplicationServiceFactory; secretTag: TSecretTagServiceFactory; @@ -281,9 +280,6 @@ declare module "fastify" { telemetry: TTelemetryServiceFactory; dynamicSecret: TDynamicSecretServiceFactory; dynamicSecretLease: TDynamicSecretLeaseServiceFactory; - projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; - identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; - identityProjectAdditionalPrivilegeV2: TIdentityProjectAdditionalPrivilegeV2ServiceFactory; secretSharing: TSecretSharingServiceFactory; rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; @@ -324,6 +320,13 @@ declare module "fastify" { pamAccount: TPamAccountServiceFactory; pamSession: TPamSessionServiceFactory; upgradePath: TUpgradePathService; + + membershipUser: TMembershipUserServiceFactory; + membershipIdentity: TMembershipIdentityServiceFactory; + membershipGroup: TMembershipGroupServiceFactory; + additionalPrivilege: TAdditionalPrivilegeServiceFactory; + role: TRoleServiceFactory; + convertor: TConvertorServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index c4d45ca27..07fe2c97d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -17,6 +17,9 @@ import { TAccessApprovalRequestsReviewersInsert, TAccessApprovalRequestsReviewersUpdate, TAccessApprovalRequestsUpdate, + TAdditionalPrivileges, + TAdditionalPrivilegesInsert, + TAdditionalPrivilegesUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -227,6 +230,15 @@ import { TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate, + TMembershipRoles, + TMembershipRolesInsert, + TMembershipRolesUpdate, + TMemberships, + TMembershipsInsert, + TMembershipsUpdate, + TNamespaces, + TNamespacesInsert, + TNamespacesUpdate, TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate, @@ -314,6 +326,9 @@ import { TResourceMetadata, TResourceMetadataInsert, TResourceMetadataUpdate, + TRoles, + TRolesInsert, + TRolesUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -1316,5 +1331,19 @@ declare module "knex/types/tables" { [TableName.PamResource]: KnexOriginal.CompositeTableType; [TableName.PamAccount]: KnexOriginal.CompositeTableType; [TableName.PamSession]: KnexOriginal.CompositeTableType; + + [TableName.Namespace]: KnexOriginal.CompositeTableType; + [TableName.Membership]: KnexOriginal.CompositeTableType; + [TableName.MembershipRole]: KnexOriginal.CompositeTableType< + TMembershipRoles, + TMembershipRolesInsert, + TMembershipRolesUpdate + >; + [TableName.Role]: KnexOriginal.CompositeTableType; + [TableName.AdditionalPrivilege]: KnexOriginal.CompositeTableType< + TAdditionalPrivileges, + TAdditionalPrivilegesInsert, + TAdditionalPrivilegesUpdate + >; } } diff --git a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts index 84824ac65..c1b6bb48b 100644 --- a/backend/src/db/migrations/20240702131735_secret-approval-groups.ts +++ b/backend/src/db/migrations/20240702131735_secret-approval-groups.ts @@ -127,7 +127,8 @@ export async function down(knex: Knex): Promise { }); await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => { tb.dropColumn("approverUserId"); - tb.uuid("approverId").notNullable().alter(); + // akhilmhdh: i had to comment this out and is not possible as membership is now changed in structure + // tb.uuid("approverId").notNullable().alter(); }); } } diff --git a/backend/src/db/migrations/20251005152640_simplify-membership.ts b/backend/src/db/migrations/20251005152640_simplify-membership.ts new file mode 100644 index 000000000..ff2987da7 --- /dev/null +++ b/backend/src/db/migrations/20251005152640_simplify-membership.ts @@ -0,0 +1,1121 @@ +import { Knex } from "knex"; + +import { AccessScope, TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +const createNamespaceTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.Namespace, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("description"); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.Namespace); +}; + +const createMembershipTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.Membership, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("scope", 24).notNullable(); + + t.uuid("actorUserId"); + t.foreign("actorUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.uuid("actorIdentityId"); + t.foreign("actorIdentityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.uuid("actorGroupId"); + t.foreign("actorGroupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.uuid("scopeOrgId").notNullable(); + t.foreign("scopeOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("scopeProjectId", 36); + t.foreign("scopeProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("scopeNamespaceId"); + t.foreign("scopeNamespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.boolean("isActive").defaultTo(true).notNullable(); + t.string("status"); + t.string("inviteEmail"); + t.datetime("lastInvitedAt"); + t.string("lastLoginAuthMethod"); + t.datetime("lastLoginTime"); + t.specificType("projectFavorites", "text[]"); + t.timestamps(true, true, true); + + t.index(["scope", "scopeOrgId"]); + + t.check( + `(:actorUserIdColumn: IS NOT NULL AND :actorIdentityIdColumn: IS NULL AND :actorGroupIdColumn: IS NULL) OR + (:actorIdentityIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL AND :actorGroupIdColumn: IS NULL) OR + (:actorGroupIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL AND :actorIdentityIdColumn: IS NULL)`, + { + actorUserIdColumn: "actorUserId", + actorIdentityIdColumn: "actorIdentityId", + actorGroupIdColumn: "actorGroupId" + }, + "only_one_actor_type" + ); + + t.check( + `(:scopeColumn: = 'namespace' AND :scopeNamespaceIdColumn: IS NOT NULL) OR + (:scopeColumn: = 'project' AND :scopeProjectIdColumn: IS NOT NULL) OR + (:scopeColumn: = 'organization') + `, + { + scopeColumn: "scope", + scopeNamespaceIdColumn: "scopeNamespaceId", + scopeProjectIdColumn: "scopeProjectId" + }, + "scope_matches_id" + ); + }); + + await createOnUpdateTrigger(knex, TableName.Membership); +}; + +const createRoleTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.Role, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.string("description"); + t.string("slug").notNullable(); + t.jsonb("permissions").notNullable(); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("projectId", 36); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("namespaceId"); + t.foreign("namespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.check( + `(:orgIdColumn: IS NOT NULL AND :namespaceIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:namespaceIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:projectIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :namespaceIdColumn: IS NULL)`, + { + orgIdColumn: "orgId", + namespaceIdColumn: "namespaceId", + projectIdColumn: "projectId" + }, + "only_one_scope_id" + ); + + t.timestamps(true, true, true); + }); + + await knex.schema.raw(` + CREATE UNIQUE INDEX role_name_org_id_unique + ON "${TableName.Role}" (slug, "orgId") + WHERE "orgId" IS NOT NULL; + `); + + await knex.schema.raw(` + CREATE UNIQUE INDEX role_name_project_id_unique + ON "${TableName.Role}" (slug, "projectId") + WHERE "projectId" IS NOT NULL; + `); + + await knex.schema.raw(` + CREATE UNIQUE INDEX role_name_namespace_id_unique + ON "${TableName.Role}" (slug, "namespaceId") + WHERE "namespaceId" IS NOT NULL; + `); + + await createOnUpdateTrigger(knex, TableName.Role); +}; + +const createMembershipRoleTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.MembershipRole, (t) => { + t.uuid("id").primary().defaultTo(knex.fn.uuid()); + t.string("role").notNullable(); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + + t.uuid("customRoleId"); + t.foreign("customRoleId").references("id").inTable(TableName.Role); + t.uuid("membershipId").notNullable(); + t.foreign("membershipId").references("id").inTable(TableName.Membership).onDelete("CASCADE"); + + t.index("membershipId"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.MembershipRole); +}; + +const createAdditionalPrivilegeTable = async (knex: Knex) => { + await knex.schema.createTable(TableName.AdditionalPrivilege, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name", 60).notNullable(); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.jsonb("permissions").notNullable(); + + t.uuid("actorUserId"); + t.foreign("actorUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + t.uuid("actorIdentityId"); + t.foreign("actorIdentityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("projectId", 36); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.uuid("namespaceId"); + t.foreign("namespaceId").references("id").inTable(TableName.Namespace).onDelete("CASCADE"); + + t.check( + `(:orgIdColumn: IS NOT NULL AND :namespaceIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:namespaceIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :projectIdColumn: IS NULL) OR + (:projectIdColumn: IS NOT NULL AND :orgIdColumn: IS NULL AND :namespaceIdColumn: IS NULL)`, + { + orgIdColumn: "orgId", + namespaceIdColumn: "namespaceId", + projectIdColumn: "projectId" + }, + "only_one_scope_id" + ); + + t.check( + `(:actorUserIdColumn: IS NOT NULL AND :actorIdentityIdColumn: IS NULL) OR + (:actorIdentityIdColumn: IS NOT NULL AND :actorUserIdColumn: IS NULL) + `, + { + actorUserIdColumn: "actorUserId", + actorIdentityIdColumn: "actorIdentityId" + }, + "only_one_actor_type" + ); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.AdditionalPrivilege); +}; + +const migrateMembershipData = async (knex: Knex) => { + await knex + .insert( + knex(TableName.OrgMembership).select( + "id", + "status", + "inviteEmail", + "createdAt", + "updatedAt", + "userId", + "orgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + knex.raw("?", [AccessScope.Organization]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "status", + "inviteEmail", + "createdAt", + "updatedAt", + "actorUserId", + "scopeOrgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityOrgMembership).select( + "id", + "identityId", + "orgId", + "lastLoginAuthMethod", + "lastLoginTime", + "createdAt", + "updatedAt", + knex.raw("?", [AccessScope.Organization]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorIdentityId", + "scopeOrgId", + "lastLoginAuthMethod", + "lastLoginTime", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.Groups).select("id", "orgId", "createdAt", "updatedAt", knex.raw("?", [AccessScope.Organization])) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??)", [ + TableName.Membership, + "actorGroupId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.ProjectMembership) + .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + knex.ref("id").withSchema(TableName.ProjectMembership), + "userId", + "projectId", + "orgId", + knex.ref("createdAt").withSchema(TableName.ProjectMembership), + knex.ref("updatedAt").withSchema(TableName.ProjectMembership), + knex.raw("?", [AccessScope.Project]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorUserId", + "scopeProjectId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityProjectMembership) + .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + knex.ref("id").withSchema(TableName.IdentityProjectMembership), + "identityId", + "projectId", + "orgId", + knex.ref("createdAt").withSchema(TableName.IdentityProjectMembership), + knex.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + knex.raw("?", [AccessScope.Project]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorIdentityId", + "scopeProjectId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); + + await knex + .insert( + knex(TableName.GroupProjectMembership) + .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .select( + knex.ref("id").withSchema(TableName.GroupProjectMembership), + "groupId", + "projectId", + "orgId", + knex.ref("createdAt").withSchema(TableName.GroupProjectMembership), + knex.ref("updatedAt").withSchema(TableName.GroupProjectMembership), + knex.raw("?", [AccessScope.Project]) + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??)", [ + TableName.Membership, + "id", + "actorGroupId", + "scopeProjectId", + "scopeOrgId", + "createdAt", + "updatedAt", + "scope" + ]) + ); +}; + +const migrateRoleData = async (knex: Knex) => { + await knex + .insert( + knex(TableName.OrgRoles).select( + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "orgId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Role, + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "orgId" + ]) + ); + + await knex + .insert( + knex(TableName.ProjectRoles).select( + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "projectId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.Role, + "id", + "name", + "description", + "slug", + "permissions", + "createdAt", + "updatedAt", + "projectId" + ]) + ); + + const hasExternalGroupRoleMappingRoleColumn = await knex.schema.hasColumn( + TableName.ExternalGroupOrgRoleMapping, + "roleId" + ); + if (hasExternalGroupRoleMappingRoleColumn) { + await knex.schema.alterTable(TableName.ExternalGroupOrgRoleMapping, (t) => { + t.dropForeign("roleId"); + t.foreign("roleId").references("id").inTable(TableName.Role); + }); + } +}; + +const migrateMembershipRoleData = async (knex: Knex) => { + await knex + .insert(knex(TableName.OrgMembership).select("id", "role", "roleId")) + .into(knex.raw("?? (??, ??, ??)", [TableName.MembershipRole, "membershipId", "role", "customRoleId"])); + + await knex + .insert(knex(TableName.IdentityOrgMembership).select("id", "role", "roleId")) + .into(knex.raw("?? (??, ??, ??)", [TableName.MembershipRole, "membershipId", "role", "customRoleId"])); + + await knex + .insert( + knex(TableName.Groups) + .join(TableName.Membership, (qb) => { + qb.on(`${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .andOn(`${TableName.Groups}.orgId`, `${TableName.Membership}.scopeOrgId`) + .andOn(`${TableName.Membership}.scope`, knex.raw("?", [AccessScope.Organization])); + }) + .select(knex.ref("id").withSchema(TableName.Membership), "role", "roleId") + ) + .into(knex.raw("?? (??, ??, ??)", [TableName.MembershipRole, "membershipId", "role", "customRoleId"])); + + await knex + .insert( + knex(TableName.ProjectUserMembershipRole).select( + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.MembershipRole, + "id", + "role", + "membershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityProjectMembershipRole).select( + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.MembershipRole, + "id", + "role", + "membershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]) + ); + + await knex + .insert( + knex(TableName.GroupProjectMembershipRole).select( + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.MembershipRole, + "id", + "role", + "membershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]) + ); +}; + +const migrateAdditionalPrivilegeData = async (knex: Knex) => { + await knex + .insert( + knex(TableName.ProjectUserAdditionalPrivilege).select( + "id", + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "userId", + "projectId", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.AdditionalPrivilege, + "id", + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "actorUserId", + "projectId", + "createdAt", + "updatedAt" + ]) + ); + + await knex + .insert( + knex(TableName.IdentityProjectAdditionalPrivilege) + .join( + TableName.IdentityProjectMembership, + `${TableName.IdentityProjectMembership}.id`, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` + ) + .select( + knex.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege), + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "identityId", + "projectId", + knex.ref("createdAt").withSchema(TableName.IdentityProjectAdditionalPrivilege), + knex.ref("updatedAt").withSchema(TableName.IdentityProjectAdditionalPrivilege) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.AdditionalPrivilege, + "id", + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "actorIdentityId", + "projectId", + "createdAt", + "updatedAt" + ]) + ); + + const hasApColumnInAccessApprovalRequest = await knex.schema.hasColumn( + TableName.AccessApprovalRequest, + "privilegeId" + ); + if (hasApColumnInAccessApprovalRequest) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropForeign("privilegeId"); + t.foreign("privilegeId").references("id").inTable(TableName.AdditionalPrivilege); + }); + } +}; + +export async function up(knex: Knex): Promise { + const hasToMigrateNamespaceTable = !(await knex.schema.hasTable(TableName.Namespace)); + if (hasToMigrateNamespaceTable) { + await createNamespaceTable(knex); + } + + const hasToMigrateMembershipTable = !(await knex.schema.hasTable(TableName.Membership)); + if (hasToMigrateMembershipTable) { + await createMembershipTable(knex); + } + + const hasToMigrateRoleTable = !(await knex.schema.hasTable(TableName.Role)); + if (hasToMigrateRoleTable) { + await createRoleTable(knex); + } + + const hasToMigrateMembershipRoleTable = !(await knex.schema.hasTable(TableName.MembershipRole)); + if (hasToMigrateMembershipRoleTable) { + await createMembershipRoleTable(knex); + } + + const hasToMigrateAdditionalPrivilegeTable = !(await knex.schema.hasTable(TableName.AdditionalPrivilege)); + if (hasToMigrateAdditionalPrivilegeTable) { + await createAdditionalPrivilegeTable(knex); + } + + // this means these tables have been created before + if (hasToMigrateMembershipTable) { + await migrateMembershipData(knex); + } + + if (hasToMigrateRoleTable) { + await migrateRoleData(knex); + } + + if (hasToMigrateMembershipRoleTable) { + await migrateMembershipRoleData(knex); + } + + if (hasToMigrateAdditionalPrivilegeTable) { + await migrateAdditionalPrivilegeData(knex); + } +} + +const rollbackAdditionalPrivilegeData = async (knex: Knex) => { + const projectUserAdditionalPrivilegeFields = [ + "id", + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "userId", + "projectId", + "createdAt", + "updatedAt" + ]; + + await knex + .insert( + knex(TableName.AdditionalPrivilege) + .whereNotNull("actorUserId") + .whereNotNull("projectId") + .select( + "id", + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "actorUserId", + "projectId", + "createdAt", + "updatedAt" + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.ProjectUserAdditionalPrivilege, + ...projectUserAdditionalPrivilegeFields + ]) + ) + .onConflict("id") + .merge(projectUserAdditionalPrivilegeFields); + + const identityProjectAdditionalPrivilegeFields = [ + "id", + "slug", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + "projectMembershipId", + "createdAt", + "updatedAt" + ]; + + await knex + .insert( + knex(TableName.AdditionalPrivilege) + .join(TableName.Membership, (qb) => { + qb.on(`${TableName.AdditionalPrivilege}.actorIdentityId`, `${TableName.Membership}.actorIdentityId`) + .andOn(`${TableName.AdditionalPrivilege}.projectId`, `${TableName.Membership}.scopeProjectId`) + .andOn(`${TableName.Membership}.scope`, knex.raw("?", [AccessScope.Project])); + }) + .whereNotNull(`${TableName.AdditionalPrivilege}.actorIdentityId`) + .whereNotNull(`${TableName.AdditionalPrivilege}.projectId`) + .select( + knex.ref("id").withSchema(TableName.AdditionalPrivilege), + "name", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "permissions", + knex.ref("id").withSchema(TableName.Membership).as("projectMembershipId"), + knex.ref("createdAt").withSchema(TableName.AdditionalPrivilege), + knex.ref("updatedAt").withSchema(TableName.AdditionalPrivilege) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.IdentityProjectAdditionalPrivilege, + ...identityProjectAdditionalPrivilegeFields + ]) + ) + .onConflict("id") + .merge(identityProjectAdditionalPrivilegeFields); +}; + +const rollbackMembershipRoleData = async (knex: Knex) => { + const groupRoleFields = ["id", "name", "slug", "createdAt", "updatedAt", "role", "roleId", "orgId"]; + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .join(TableName.Groups, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .select( + knex.ref("actorGroupId").withSchema(TableName.Membership), + knex.ref("name").withSchema(TableName.Groups), + knex.ref("slug").withSchema(TableName.Groups), + knex.ref("createdAt").withSchema(TableName.Groups), + knex.ref("updatedAt").withSchema(TableName.Groups), + knex.ref("role").withSchema(TableName.MembershipRole), + "customRoleId", + knex.ref("orgId").withSchema(TableName.Groups) + ) + ) + .into(knex.raw("?? (??,??,??,??,??,??,??,??)", [TableName.Groups, ...groupRoleFields])) + .onConflict("id") + .merge(groupRoleFields); + + const projectMembershipRoleFields = [ + "id", + "role", + "projectMembershipId", + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + "createdAt", + "updatedAt" + ]; + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .select( + knex.ref("id").withSchema(TableName.MembershipRole), + "role", + knex.ref("membershipId").withSchema(TableName.MembershipRole), + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + knex.ref("createdAt").withSchema(TableName.MembershipRole), + knex.ref("updatedAt").withSchema(TableName.MembershipRole) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.ProjectUserMembershipRole, + ...projectMembershipRoleFields + ]) + ) + .onConflict("id") + .merge(projectMembershipRoleFields); + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .select( + knex.ref("id").withSchema(TableName.MembershipRole), + "role", + knex.ref("membershipId").withSchema(TableName.MembershipRole), + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + knex.ref("createdAt").withSchema(TableName.MembershipRole), + knex.ref("updatedAt").withSchema(TableName.MembershipRole) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.IdentityProjectMembershipRole, + ...projectMembershipRoleFields + ]) + ) + .onConflict("id") + .merge(projectMembershipRoleFields); + + await knex + .insert( + knex(TableName.MembershipRole) + .join(TableName.Membership, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .select( + knex.ref("id").withSchema(TableName.MembershipRole), + "role", + knex.ref("membershipId").withSchema(TableName.MembershipRole), + "customRoleId", + "isTemporary", + "temporaryMode", + "temporaryRange", + "temporaryAccessStartTime", + "temporaryAccessEndTime", + knex.ref("createdAt").withSchema(TableName.MembershipRole), + knex.ref("updatedAt").withSchema(TableName.MembershipRole) + ) + ) + .into( + knex.raw("?? (??,??,??,??,??,??,??,??,??,??,??)", [ + TableName.GroupProjectMembershipRole, + ...projectMembershipRoleFields + ]) + ) + .onConflict("id") + .merge(projectMembershipRoleFields); +}; + +const rollbackRoleData = async (knex: Knex) => { + const orgRoleFields = ["id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "orgId"]; + + await knex + .insert( + knex(TableName.Role) + .whereNotNull("orgId") + .select("id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "orgId") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [TableName.OrgRoles, ...orgRoleFields])) + .onConflict("id") + .merge(orgRoleFields); + + const projectRoleFields = ["id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "projectId"]; + + await knex + .insert( + knex(TableName.Role) + .whereNotNull("projectId") + .select("id", "name", "description", "slug", "permissions", "createdAt", "updatedAt", "projectId") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??)", [TableName.ProjectRoles, ...projectRoleFields])) + .onConflict("id") + .merge(projectRoleFields); +}; + +const rollbackMembershipData = async (knex: Knex) => { + const orgMembershipFields = [ + "id", + "status", + "inviteEmail", + "createdAt", + "updatedAt", + "userId", + "orgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + "role", + "roleId" + ]; + await knex + .insert( + knex(TableName.Membership) + .leftJoin(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .select( + knex.ref("id").withSchema(TableName.Membership), + "status", + "inviteEmail", + knex.ref("createdAt").withSchema(TableName.Membership), + knex.ref("updatedAt").withSchema(TableName.Membership), + "actorUserId", + "scopeOrgId", + "projectFavorites", + "isActive", + "lastInvitedAt", + "lastLoginAuthMethod", + "lastLoginTime", + "role", + "customRoleId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.OrgMembership, + ...orgMembershipFields + ]) + ) + .onConflict("id") + .merge(orgMembershipFields); + + const identityOrgMembershipFields = [ + "id", + "identityId", + "orgId", + "lastLoginAuthMethod", + "lastLoginTime", + "createdAt", + "updatedAt", + "role", + "roleId" + ]; + + await knex + .insert( + knex(TableName.Membership) + .leftJoin(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .select( + knex.ref("id").withSchema(TableName.Membership), + "actorIdentityId", + "scopeOrgId", + "lastLoginAuthMethod", + "lastLoginTime", + knex.ref("createdAt").withSchema(TableName.Membership), + knex.ref("updatedAt").withSchema(TableName.Membership), + "role", + "customRoleId" + ) + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??, ??, ??, ??, ??)", [ + TableName.IdentityOrgMembership, + ...identityOrgMembershipFields + ]) + ) + .onConflict("id") + .merge(identityOrgMembershipFields); + + const projectMembershipFields = ["id", "userId", "projectId", "createdAt", "updatedAt"]; + + await knex + .insert( + knex(TableName.Membership) + .where("scope", AccessScope.Project) + .whereNotNull("actorUserId") + .select("id", "actorUserId", "scopeProjectId", "createdAt", "updatedAt") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??)", [TableName.ProjectMembership, ...projectMembershipFields])) + .onConflict("id") + .merge(projectMembershipFields); + + const identityProjectMembershipFields = ["id", "identityId", "projectId", "createdAt", "updatedAt"]; + + await knex + .insert( + knex(TableName.Membership) + .where("scope", AccessScope.Project) + .whereNotNull("actorIdentityId") + .select("id", "actorIdentityId", "scopeProjectId", "createdAt", "updatedAt") + ) + .into( + knex.raw("?? (??, ??, ??, ??, ??)", [TableName.IdentityProjectMembership, ...identityProjectMembershipFields]) + ) + .onConflict("id") + .merge(identityProjectMembershipFields); + + const groupProjectMembershipFields = ["id", "groupId", "projectId", "createdAt", "updatedAt"]; + + await knex + .insert( + knex(TableName.Membership) + .where("scope", AccessScope.Project) + .whereNotNull("actorGroupId") + .select("id", "actorGroupId", "scopeProjectId", "createdAt", "updatedAt") + ) + .into(knex.raw("?? (??, ??, ??, ??, ??)", [TableName.GroupProjectMembership, ...groupProjectMembershipFields])) + .onConflict("id") + .merge(groupProjectMembershipFields); +}; + +export async function down(knex: Knex): Promise { + const hasRoleTable = await knex.schema.hasTable(TableName.Role); + if (hasRoleTable) { + await rollbackRoleData(knex); + } + + const hasMembershipTable = await knex.schema.hasTable(TableName.Membership); + if (hasMembershipTable) { + await rollbackMembershipData(knex); + } + + const hasMembershipRoleTable = await knex.schema.hasTable(TableName.MembershipRole); + if (hasMembershipRoleTable) { + await rollbackMembershipRoleData(knex); + } + + const hasAdditionalPrivilegeTable = await knex.schema.hasTable(TableName.AdditionalPrivilege); + if (hasAdditionalPrivilegeTable) { + await rollbackAdditionalPrivilegeData(knex); + } + + // Restore foreign key references + const hasApColumnInAccessApprovalRequest = await knex.schema.hasColumn( + TableName.AccessApprovalRequest, + "privilegeId" + ); + if (hasApColumnInAccessApprovalRequest) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => { + t.dropForeign("privilegeId"); + t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege); + }); + } + + const hasExternalGroupRoleMappingRoleColumn = await knex.schema.hasColumn( + TableName.ExternalGroupOrgRoleMapping, + "roleId" + ); + if (hasExternalGroupRoleMappingRoleColumn) { + await knex.schema.alterTable(TableName.ExternalGroupOrgRoleMapping, (t) => { + t.dropForeign("roleId"); + t.foreign("roleId").references("id").inTable(TableName.OrgRoles); + }); + } + + // Drop new tables + await dropOnUpdateTrigger(knex, TableName.AdditionalPrivilege); + await knex.schema.dropTableIfExists(TableName.AdditionalPrivilege); + + await dropOnUpdateTrigger(knex, TableName.MembershipRole); + await knex.schema.dropTableIfExists(TableName.MembershipRole); + + await dropOnUpdateTrigger(knex, TableName.Membership); + await knex.schema.dropTableIfExists(TableName.Membership); + + await dropOnUpdateTrigger(knex, TableName.Role); + await knex.schema.dropTableIfExists(TableName.Role); + + await dropOnUpdateTrigger(knex, TableName.Namespace); + await knex.schema.dropTableIfExists(TableName.Namespace); +} diff --git a/backend/src/db/schemas/additional-privileges.ts b/backend/src/db/schemas/additional-privileges.ts new file mode 100644 index 000000000..8013a6961 --- /dev/null +++ b/backend/src/db/schemas/additional-privileges.ts @@ -0,0 +1,30 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AdditionalPrivilegesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + permissions: z.unknown(), + actorUserId: z.string().uuid().nullable().optional(), + actorIdentityId: z.string().uuid().nullable().optional(), + orgId: z.string().uuid().nullable().optional(), + projectId: z.string().nullable().optional(), + namespaceId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAdditionalPrivileges = z.infer; +export type TAdditionalPrivilegesInsert = Omit, TImmutableDBKeys>; +export type TAdditionalPrivilegesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index f8ac885b4..2a10e0f1b 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -3,6 +3,7 @@ export * from "./access-approval-policies-approvers"; export * from "./access-approval-policies-bypassers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; +export * from "./additional-privileges"; export * from "./api-keys"; export * from "./app-connections"; export * from "./audit-log-streams"; @@ -73,8 +74,11 @@ export * from "./kms-keys"; export * from "./kms-root-config"; export * from "./ldap-configs"; export * from "./ldap-group-maps"; +export * from "./membership-roles"; +export * from "./memberships"; export * from "./microsoft-teams-integrations"; export * from "./models"; +export * from "./namespaces"; export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-gateway-config"; @@ -108,6 +112,7 @@ export * from "./projects"; export * from "./rate-limit"; export * from "./relays"; export * from "./resource-metadata"; +export * from "./roles"; export * from "./saml-configs"; export * from "./scim-tokens"; export * from "./secret-approval-policies"; diff --git a/backend/src/db/schemas/membership-roles.ts b/backend/src/db/schemas/membership-roles.ts new file mode 100644 index 000000000..98927c9cb --- /dev/null +++ b/backend/src/db/schemas/membership-roles.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const MembershipRolesSchema = z.object({ + id: z.string().uuid(), + role: z.string(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + customRoleId: z.string().uuid().nullable().optional(), + membershipId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMembershipRoles = z.infer; +export type TMembershipRolesInsert = Omit, TImmutableDBKeys>; +export type TMembershipRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/memberships.ts b/backend/src/db/schemas/memberships.ts new file mode 100644 index 000000000..e871a5627 --- /dev/null +++ b/backend/src/db/schemas/memberships.ts @@ -0,0 +1,32 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const MembershipsSchema = z.object({ + id: z.string().uuid(), + scope: z.string(), + actorUserId: z.string().uuid().nullable().optional(), + actorIdentityId: z.string().uuid().nullable().optional(), + actorGroupId: z.string().uuid().nullable().optional(), + scopeOrgId: z.string().uuid(), + scopeProjectId: z.string().nullable().optional(), + scopeNamespaceId: z.string().uuid().nullable().optional(), + isActive: z.boolean().default(true), + status: z.string().nullable().optional(), + inviteEmail: z.string().nullable().optional(), + lastInvitedAt: z.date().nullable().optional(), + lastLoginAuthMethod: z.string().nullable().optional(), + lastLoginTime: z.date().nullable().optional(), + projectFavorites: z.string().array().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TMemberships = z.infer; +export type TMembershipsInsert = Omit, TImmutableDBKeys>; +export type TMembershipsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 09ecb367a..d3537b028 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -178,6 +178,14 @@ export enum TableName { SecretScanningScan = "secret_scanning_scans", SecretScanningFinding = "secret_scanning_findings", SecretScanningConfig = "secret_scanning_configs", + + Membership = "memberships", + MembershipRole = "membership_roles", + Role = "roles", + AdditionalPrivilege = "additional_privileges", + + Namespace = "namespaces", + // reminders Reminder = "reminders", ReminderRecipient = "reminders_recipients", @@ -302,7 +310,39 @@ export enum ActionProjectType { Any = "any" } +export enum TemporaryPermissionMode { + Relative = "relative" +} + +export enum MembershipActors { + Group = "group", + User = "user", + Identity = "identity" +} + export enum SortDirection { ASC = "asc", DESC = "desc" } + +export enum AccessScope { + Organization = "organization", + Namespace = "namespace", + Project = "project" +} + +export type AccessScopeData = + | { + scope: AccessScope.Organization; + orgId: string; + } + | { + scope: AccessScope.Namespace; + orgId: string; + namespaceId: string; + } + | { + scope: AccessScope.Project; + orgId: string; + projectId: string; + }; diff --git a/backend/src/db/schemas/namespaces.ts b/backend/src/db/schemas/namespaces.ts new file mode 100644 index 000000000..b9f092fd3 --- /dev/null +++ b/backend/src/db/schemas/namespaces.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const NamespacesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TNamespaces = z.infer; +export type TNamespacesInsert = Omit, TImmutableDBKeys>; +export type TNamespacesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/roles.ts b/backend/src/db/schemas/roles.ts new file mode 100644 index 000000000..4801a57a6 --- /dev/null +++ b/backend/src/db/schemas/roles.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const RolesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + description: z.string().nullable().optional(), + slug: z.string(), + permissions: z.unknown(), + orgId: z.string().uuid().nullable().optional(), + projectId: z.string().nullable().optional(), + namespaceId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TRoles = z.infer; +export type TRolesInsert = Omit, TImmutableDBKeys>; +export type TRolesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/user-notifications-default.ts b/backend/src/db/schemas/user-notifications-default.ts new file mode 100644 index 000000000..eabcdabc5 --- /dev/null +++ b/backend/src/db/schemas/user-notifications-default.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const UserNotificationsDefaultSchema = z.object({ + id: z.string().uuid(), + userId: z.string().uuid(), + orgId: z.string().uuid().nullable().optional(), + type: z.string(), + title: z.string(), + body: z.string().nullable().optional(), + link: z.string().nullable().optional(), + isRead: z.boolean().default(false), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TUserNotificationsDefault = z.infer; +export type TUserNotificationsDefaultInsert = Omit, TImmutableDBKeys>; +export type TUserNotificationsDefaultUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/seeds/2-org.ts b/backend/src/db/seeds/2-org.ts index a02224dbc..99bd70f57 100644 --- a/backend/src/db/seeds/2-org.ts +++ b/backend/src/db/seeds/2-org.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; +import { AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export async function seed(knex: Knex): Promise { @@ -24,13 +24,22 @@ export async function seed(knex: Knex): Promise { ]) .returning("*"); - await knex(TableName.OrgMembership).insert([ + const [membership] = await knex(TableName.Membership) + .insert([ + { + scope: AccessScope.Organization, + scopeOrgId: org.id, + actorUserId: user.id, + isActive: true, + status: OrgMembershipStatus.Accepted + } + ]) + .returning("*"); + + await knex(TableName.MembershipRole).insert([ { - role: OrgMembershipRole.Admin, - orgId: org.id, - status: OrgMembershipStatus.Accepted, - userId: user.id, - isActive: true + membershipId: membership.id, + role: OrgMembershipRole.Admin } ]); } diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 47a41a95c..d0294022f 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -6,14 +6,15 @@ import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { initLogger, logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AuthMethod } from "@app/services/auth/auth-type"; +import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"; -import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; -import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { userDALFactory } from "@app/services/user/user-dal"; import { + AccessScope, OrgMembershipRole, OrgMembershipStatus, ProjectMembershipRole, @@ -39,8 +40,8 @@ const createUserWithGhostUser = async ( ) => { const projectKeyDAL = projectKeyDALFactory(knex); const userDAL = userDALFactory(knex); - const projectMembershipDAL = projectMembershipDALFactory(knex); - const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex); + const membershipDAL = membershipUserDALFactory(knex); + const membershipRoleDAL = membershipRoleDALFactory(knex); const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. @@ -63,25 +64,36 @@ const createUserWithGhostUser = async ( .onConflict("userId") .merge(); - await knex(TableName.OrgMembership) + const [orgMembership] = await knex(TableName.Membership) .insert({ - orgId, - userId: ghostUser.id, - role: OrgMembershipRole.Admin, + scope: AccessScope.Organization, + scopeOrgId: orgId, + actorUserId: ghostUser.id, status: OrgMembershipStatus.Accepted, isActive: true }) .returning("*"); - const [projectMembership] = await knex(TableName.ProjectMembership) + await knex(TableName.MembershipRole).insert([ + { + membershipId: orgMembership.id, + role: OrgMembershipRole.Admin + } + ]); + + const [projectMembership] = await knex(TableName.Membership) .insert({ - userId: ghostUser.id, - projectId + actorUserId: ghostUser.id, + scopeProjectId: projectId, + scope: AccessScope.Project, + scopeOrgId: orgId, + status: OrgMembershipStatus.Accepted, + isActive: true }) .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ - projectMembershipId: projectMembership.id, + await knex(TableName.MembershipRole).insert({ + membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }); @@ -142,17 +154,16 @@ const createUserWithGhostUser = async ( }); // Create a membership for the user - const userProjectMembership = await projectMembershipDAL.create( + const userProjectMembership = await membershipDAL.create( { - projectId, - userId: user.id + scopeProjectId: projectId, + scope: AccessScope.Project, + actorUserId: user.id, + scopeOrgId: orgId }, knex ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, - knex - ); + await membershipRoleDAL.create({ membershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, knex); // Create a project key for the user await projectKeyDAL.create( @@ -195,10 +206,11 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const userOrgMembership = await knex(TableName.OrgMembership) + const userOrgMembership = await knex(TableName.Membership) .where({ - orgId: seedData1.organization.id, - userId: seedData1.id + scopeOrgId: seedData1.organization.id, + actorUserId: seedData1.id, + scope: AccessScope.Organization }) .first(); diff --git a/backend/src/db/seeds/4-project-v3.ts b/backend/src/db/seeds/4-project-v3.ts index f89b965a6..920497a82 100644 --- a/backend/src/db/seeds/4-project-v3.ts +++ b/backend/src/db/seeds/4-project-v3.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; +import { AccessScope, ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ @@ -23,15 +23,17 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const projectMembershipV3 = await knex(TableName.ProjectMembership) + const projectMembershipV3 = await knex(TableName.Membership) .insert({ - projectId: projectV2.id, - userId: seedData1.id + scopeProjectId: projectV2.id, + actorUserId: seedData1.id, + scope: AccessScope.Project, + scopeOrgId: seedData1.organization.id }) .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: projectMembershipV3[0].id + membershipId: projectMembershipV3[0].id }); // create default environments and default folders diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index ae3d04514..333fc7e3a 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -5,13 +5,12 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { initLogger, logger } from "@app/lib/logger"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; -import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; +import { AccessScope, IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; export async function seed(knex: Knex): Promise { // Deletes ALL existing entries await knex(TableName.Identity).del(); - await knex(TableName.IdentityOrgMembership).del(); initLogger(); @@ -78,34 +77,47 @@ export async function seed(knex: Knex): Promise { isClientSecretRevoked: false } ]); - await knex(TableName.IdentityOrgMembership).insert([ + const [orgMembership] = await knex(TableName.Membership) + .insert([ + { + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Organization + } + ]) + .returning("*"); + await knex(TableName.MembershipRole).insert([ { - identityId: seedData1.machineIdentity.id, - orgId: seedData1.organization.id, + membershipId: orgMembership.id, role: OrgMembershipRole.Admin } ]); - const identityProjectMembership = await knex(TableName.IdentityProjectMembership) + const identityProjectMembership = await knex(TableName.Membership) .insert({ - identityId: seedData1.machineIdentity.id, - projectId: seedData1.project.id + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Project, + scopeProjectId: seedData1.project.id }) .returning("*"); - await knex(TableName.IdentityProjectMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: identityProjectMembership[0].id + membershipId: identityProjectMembership[0].id }); - const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership) + + const identityProjectMembershipV3 = await knex(TableName.Membership) .insert({ - identityId: seedData1.machineIdentity.id, - projectId: seedData1.projectV3.id + actorIdentityId: seedData1.machineIdentity.id, + scopeOrgId: seedData1.organization.id, + scope: AccessScope.Project, + scopeProjectId: seedData1.projectV3.id }) .returning("*"); - await knex(TableName.IdentityProjectMembershipRole).insert({ + await knex(TableName.MembershipRole).insert({ role: ProjectMembershipRole.Admin, - projectMembershipId: identityProjectMembershipV3[0].id + membershipId: identityProjectMembershipV3[0].id }); } diff --git a/backend/src/ee/routes/v1/deprecated-project-role-router.ts b/backend/src/ee/routes/v1/deprecated-project-role-router.ts index dc361626d..f8f73579e 100644 --- a/backend/src/ee/routes/v1/deprecated-project-role-router.ts +++ b/backend/src/ee/routes/v1/deprecated-project-role-router.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { backfillPermissionV1SchemaToV2Schema, @@ -13,7 +13,6 @@ import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ @@ -55,14 +54,16 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)) ); - const role = await server.services.projectRole.createRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId + }); + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId }, data: { ...req.body, @@ -73,7 +74,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId, event: { type: EventType.CREATE_PROJECT_ROLE, metadata: { @@ -86,7 +87,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -131,12 +132,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) : undefined; - const role = await server.services.projectRole.updateRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId, + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId + }); + + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + selector: { + id: req.params.roleId + }, data: { ...req.body, permissions: stringifiedPermissions @@ -146,7 +156,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.UPDATE_PROJECT_ROLE, metadata: { @@ -159,7 +169,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -188,18 +198,27 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.deleteRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId + }); + + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + selector: { + id: req.params.roleId + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.DELETE_PROJECT_ROLE, metadata: { @@ -210,7 +229,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -238,17 +257,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectRole.listRoles({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug - } + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId }); - return { roles }; + + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + data: {} + }); + return { roles: roles.map((el) => ({ ...el, projectId: el.projectId as string })) }; } }); @@ -265,78 +288,30 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }), response: { 200: z.object({ - role: SanitizedRoleSchemaV1.omit({ version: true }) + role: SanitizedRoleSchemaV1 }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.getRoleBySlug({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.SLUG, - projectSlug: req.params.projectSlug - }, - roleSlug: req.params.slug + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.params.projectSlug, + orgId: req.permission.orgId }); - return { role }; - } - }); - - server.route({ - method: "GET", - url: "/:projectId/permissions", - config: { - rateLimit: readLimit - }, - schema: { - params: z.object({ - projectId: z.string().trim() - }), - response: { - 200: z.object({ - data: z.object({ - membership: z.object({ - id: z.string(), - roles: z - .object({ - role: z.string() - }) - .array() - }), - assumedPrivilegeDetails: z - .object({ - actorId: z.string(), - actorType: z.string(), - actorName: z.string(), - actorEmail: z.string().optional() - }) - .optional(), - permissions: z.any().array() - }) - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission( - req.permission.id, - req.params.projectId, - req.permission.authMethod, - req.permission.orgId - ); - - return { - data: { - permissions, - membership, - assumedPrivilegeDetails + const role = await server.services.role.getRoleBySlug({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId + }, + selector: { + slug: req.params.slug } - }; + }); + + return { role: { ...role, projectId: role.projectId as string } }; } }); }; diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index f64d3c979..5a6cea20f 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { UnauthorizedError } from "@app/lib/errors"; @@ -15,7 +15,7 @@ import { ProjectSpecificPrivilegePermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -56,6 +56,10 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F if (!permissions && !privilegePermission) { throw new UnauthorizedError({ message: "Permission or privilegePermission must be provided" }); } + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug + }); const permission = privilegePermission ? privilegePermission.actions.map((action) => ({ @@ -64,19 +68,35 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F conditions: privilegePermission.conditions })) : permissions!; - const privilege = await server.services.identityProjectAdditionalPrivilege.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - ...req.body, - slug: req.body.slug ?? slugify(alphaNumericNanoId(12)), - isTemporary: false, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - permissions: backfillPermissionV1SchemaToV2Schema(permission) + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + data: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + ...req.body, + isTemporary: false, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: backfillPermissionV1SchemaToV2Schema(permission) + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -106,7 +126,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission ).optional(), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), temporaryRange: z .string() @@ -138,19 +158,39 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F })) : permissions!; - const privilege = await server.services.identityProjectAdditionalPrivilege.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - ...req.body, - slug: req.body.slug ?? slugify(alphaNumericNanoId(12)), - isTemporary: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - permissions: backfillPermissionV1SchemaToV2Schema(permission) + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug }); - return { privilege }; + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + data: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + ...req.body, + isTemporary: true, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: backfillPermissionV1SchemaToV2Schema(permission) + } + }); + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -183,7 +223,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ).optional(), isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), temporaryRange: z .string() @@ -216,18 +256,36 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F conditions: privilegePermission.conditions })) : permissions!; - const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - slug: req.body.privilegeSlug, - identityId: req.body.identityId, - projectSlug: req.body.projectSlug, + + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug + }); + + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeNameToDoc( + req.body.privilegeSlug, + projectId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.updateAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + id: privilegeDoc.id + }, data: { + ...req.body, ...updatedInfo, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts permissions: permission ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts @@ -235,7 +293,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F : undefined } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -267,16 +334,39 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - slug: req.body.privilegeSlug, - identityId: req.body.identityId, - projectSlug: req.body.projectSlug + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.body.projectSlug }); - return { privilege }; + + const { privilegeId } = await server.services.convertor.additionalPrivilegeNameToDoc( + req.body.privilegeSlug, + projectId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.deleteAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + id: privilegeId + } + }); + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -310,15 +400,39 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({ - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - slug: req.params.privilegeSlug, - ...req.query + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.query.projectSlug }); - return { privilege }; + + const { privilegeId } = await server.services.convertor.additionalPrivilegeNameToDoc( + req.params.privilegeSlug, + projectId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.query.identityId, + actorType: ActorType.IDENTITY, + id: privilegeId + } + }); + + return { + privilege: { + ...privilege, + identityId: req.query.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + } + }; } }); @@ -349,15 +463,32 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.query + const { id: projectId } = await server.services.convertor.projectSlugToId({ + orgId: req.permission.orgId, + slug: req.query.projectSlug }); + + const { additionalPrivileges: privileges } = await server.services.additionalPrivilege.listAdditionalPrivileges({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.query.identityId, + actorType: ActorType.IDENTITY + } + }); + return { - privileges + privileges: privileges.map((privilege) => ({ + ...privilege, + identityId: req.query.identityId, + projectMembershipId: projectId, + projectId, + slug: privilege.name + })) }; } }); diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 070462d47..5a8f03038 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -1,7 +1,9 @@ +import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -25,8 +27,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { ), name: z.string().trim(), description: z.string().trim().nullish(), - // TODO(scott): once UI refactored permissions: OrgPermissionSchema.array() - permissions: z.any().array() + permissions: OrgPermissionSchema.array() }), response: { 200: z.object({ @@ -36,13 +37,18 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.createRole( - req.permission.id, - req.params.organizationId, - req.body, - req.permission.authMethod, - req.permission.orgId - ); + const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + data: { + ...req.body, + permissions: stringifiedPermissions + } + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -59,7 +65,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -82,14 +88,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.getRole( - req.permission.id, - req.params.organizationId, - req.params.roleId, - req.permission.authMethod, - req.permission.orgId - ); - return { role }; + const role = await server.services.role.getRoleById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + selector: { + id: req.params.roleId + } + }); + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -114,8 +123,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { .optional(), name: z.string().trim().optional(), description: z.string().trim().nullish(), - // TODO(scott): once UI refactored permissions: OrgPermissionSchema.array().optional() - permissions: z.any().array().optional() + permissions: OrgPermissionSchema.array().optional() }), response: { 200: z.object({ @@ -125,14 +133,21 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.updateRole( - req.permission.id, - req.params.organizationId, - req.params.roleId, - req.body, - req.permission.authMethod, - req.permission.orgId - ); + const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + selector: { + id: req.params.roleId + }, + data: { + ...req.body, + permissions: stringifiedPermissions + } + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -149,7 +164,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -172,13 +187,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const role = await server.services.orgRole.deleteRole( - req.permission.id, - req.params.organizationId, - req.params.roleId, - req.permission.authMethod, - req.permission.orgId - ); + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.params.organizationId + }, + selector: { + id: req.params.roleId + } + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, @@ -189,7 +207,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, orgId: role.orgId as string } }; } }); @@ -206,22 +224,26 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ data: z.object({ - roles: OrgRolesSchema.omit({ permissions: true }) - .merge(z.object({ permissions: z.unknown() })) - .array() + roles: OrgRolesSchema.omit({ permissions: true }).array() }) }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const roles = await server.services.orgRole.listRoles( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.orgId - ); - return { data: { roles } }; + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: {} + }); + return { + data: { + roles: roles.map((el) => ({ ...el, orgId: el.orgId as string })) + } + }; } }); @@ -237,20 +259,33 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - membership: OrgMembershipsSchema, + memberships: z + .object({ + id: z.string(), + roles: z + .object({ + role: z.string() + }) + .array() + }) + .array(), permissions: z.any().array() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { permissions, membership } = await server.services.orgRole.getUserPermission( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.orgId - ); - return { permissions, membership }; + const { permissions, memberships } = await server.services.role.getUserPermission({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + } + }); + return { + permissions, + memberships + }; } }); }; diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 5a20ad893..acf34cb3b 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; @@ -11,7 +11,6 @@ import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ @@ -55,13 +54,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { handler: async (req) => { const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); - const role = await server.services.projectRole.createRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, data: { @@ -73,7 +70,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.CREATE_PROJECT_ROLE, metadata: { @@ -86,7 +83,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -133,12 +130,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; - const role = await server.services.projectRole.updateRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId, + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + }, data: { ...req.body, permissions: stringifiedPermissions @@ -148,7 +149,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.UPDATE_PROJECT_ROLE, metadata: { @@ -161,7 +162,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -192,18 +193,22 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.deleteRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.DELETE_PROJECT_ROLE, metadata: { @@ -214,7 +219,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -244,17 +249,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectRole.listRoles({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId - } + }, + data: {} }); - return { roles }; + return { roles: roles.map((el) => ({ ...el, projectId: el.projectId as string })) }; } }); @@ -273,24 +277,25 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - role: SanitizedRoleSchema.omit({ version: true }) + role: SanitizedRoleSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.getRoleBySlug({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.getRoleBySlug({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, - roleSlug: req.params.roleSlug + selector: { + slug: req.params.roleSlug + } }); - return { role }; + + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -307,14 +312,16 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ data: z.object({ - membership: z.object({ - id: z.string(), - roles: z - .object({ - role: z.string() - }) - .array() - }), + memberships: z + .object({ + id: z.string(), + roles: z + .object({ + role: z.string() + }) + .array() + }) + .array(), assumedPrivilegeDetails: z .object({ actorId: z.string(), @@ -330,17 +337,19 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission( - req.permission.id, - req.params.projectId, - req.permission.authMethod, - req.permission.orgId - ); + const { permissions, memberships, assumedPrivilegeDetails } = await server.services.role.getUserPermission({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: req.params.projectId, + orgId: req.permission.orgId + } + }); return { data: { permissions, - membership, + memberships, assumedPrivilegeDetails } }; diff --git a/backend/src/ee/routes/v1/secret-router.ts b/backend/src/ee/routes/v1/secret-router.ts index b9eeb7729..9b7f9f35f 100644 --- a/backend/src/ee/routes/v1/secret-router.ts +++ b/backend/src/ee/routes/v1/secret-router.ts @@ -11,7 +11,6 @@ const AccessListEntrySchema = z .object({ allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(), id: z.string(), - membershipId: z.string(), name: z.string() }) .array(); diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index df8512ada..926b22231 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,17 +1,18 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/sanitizedSchema/user-additional-privilege"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -34,7 +35,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr z.object({ isTemporary: z.literal(true), temporaryMode: z - .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), temporaryRange: z .string() @@ -55,17 +56,31 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.create({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - projectMembershipId: req.body.projectMembershipId, - ...req.body.type, - slug: req.body.slug || slugify(alphaNumericNanoId(8)), - permissions: req.body.permissions + const { userId, membership } = await server.services.convertor.userMembershipIdToUserId( + req.body.projectMembershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: membership.scopeProjectId as string, + orgId: req.permission.orgId + }, + data: { + actorId: userId, + actorType: ActorType.USER, + ...req.body.type, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + permissions: req.body.permissions + } }); - return { privilege }; + + return { + privilege: { ...privilege, userId, projectId: membership.scopeProjectId as string, slug: privilege.name } + }; } }); @@ -91,7 +106,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr z.object({ isTemporary: z.literal(true).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z - .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), temporaryRange: z .string() @@ -113,21 +128,41 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.updateById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - ...req.body, - ...req.body.type, - permissions: req.body.permissions - ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - req.body.permissions - : undefined, - privilegeId: req.params.privilegeId + const data = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.privilegeId); + if (!data.privilege.actorUserId) + throw new NotFoundError({ message: `Privilege with id ${req.params.privilegeId} not found` }); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.updateAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: data.privilege.projectId as string, + orgId: req.permission.orgId + }, + data: { + ...req.body, + ...req.body.type, + permissions: req.body.permissions + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + req.body.permissions + : undefined + }, + selector: { + id: req.params.privilegeId, + actorId: data.privilege.actorUserId, + actorType: ActorType.USER + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + userId: data.privilege.actorUserId, + projectId: data.privilege.projectId as string, + slug: privilege.name + } + }; } }); @@ -149,14 +184,32 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.deleteById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - privilegeId: req.params.privilegeId + const data = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.privilegeId); + if (!data.privilege.actorUserId) + throw new NotFoundError({ message: `Privilege with id ${req.params.privilegeId} not found` }); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.deleteAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: data.privilege.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.privilegeId, + actorId: data.privilege.actorUserId, + actorType: ActorType.USER + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + userId: data.privilege.actorUserId, + projectId: data.privilege.projectId as string, + slug: privilege.name + } + }; } }); @@ -178,14 +231,33 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privileges = await server.services.projectUserAdditionalPrivilege.listPrivileges({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - projectMembershipId: req.query.projectMembershipId + const { userId, membership } = await server.services.convertor.userMembershipIdToUserId( + req.query.projectMembershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { additionalPrivileges: privileges } = await server.services.additionalPrivilege.listAdditionalPrivileges({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: membership.scopeProjectId as string, + orgId: req.permission.orgId + }, + selector: { + actorId: userId, + actorType: ActorType.USER + } }); - return { privileges }; + + return { + privileges: privileges.map((privilege) => ({ + ...privilege, + userId: membership.actorUserId as string, + projectId: membership.scopeProjectId as string, + slug: privilege.name + })) + }; } }); @@ -207,14 +279,32 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const privilege = await server.services.projectUserAdditionalPrivilege.getPrivilegeDetailsById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - privilegeId: req.params.privilegeId + const data = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.privilegeId); + if (!data.privilege.actorUserId) + throw new NotFoundError({ message: `Privilege with id ${req.params.privilegeId} not found` }); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: data.privilege.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.privilegeId, + actorId: data.privilege.actorUserId, + actorType: ActorType.USER + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + userId: data.privilege.actorUserId, + projectId: data.privilege.projectId as string, + slug: privilege.name + } + }; } }); }; diff --git a/backend/src/ee/routes/v2/deprecated-project-role-router.ts b/backend/src/ee/routes/v2/deprecated-project-role-router.ts index 326bda06a..0a44d4d21 100644 --- a/backend/src/ee/routes/v2/deprecated-project-role-router.ts +++ b/backend/src/ee/routes/v2/deprecated-project-role-router.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { z } from "zod"; -import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; @@ -11,7 +11,6 @@ import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types"; export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProvider) => { server.route({ @@ -55,13 +54,11 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv handler: async (req) => { const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); - const role = await server.services.projectRole.createRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.createRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, data: { @@ -73,7 +70,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: req.params.projectId, event: { type: EventType.CREATE_PROJECT_ROLE, metadata: { @@ -86,7 +83,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -133,12 +130,16 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; - const role = await server.services.projectRole.updateRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId, + const role = await server.services.role.updateRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + }, data: { ...req.body, permissions: stringifiedPermissions @@ -148,7 +149,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.UPDATE_PROJECT_ROLE, metadata: { @@ -161,7 +162,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -192,18 +193,22 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.deleteRole({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - roleId: req.params.roleId + const role = await server.services.role.deleteRole({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + id: req.params.roleId + } }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: role.projectId, + projectId: role.projectId as string, event: { type: EventType.DELETE_PROJECT_ROLE, metadata: { @@ -214,7 +219,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv } }); - return { role }; + return { role: { ...role, projectId: role.projectId as string } }; } }); @@ -244,17 +249,16 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectRole.listRoles({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const { roles } = await server.services.role.listRoles({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId - } + }, + data: {} }); - return { roles }; + return { roles: roles.map((el) => ({ ...el, projectId: el.projectId as string })) }; } }); @@ -273,24 +277,25 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv }), response: { 200: z.object({ - role: SanitizedRoleSchema.omit({ version: true }) + role: SanitizedRoleSchema }) } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const role = await server.services.projectRole.getRoleBySlug({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - filter: { - type: ProjectRoleServiceIdentifierType.ID, + const role = await server.services.role.getRoleBySlug({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, projectId: req.params.projectId }, - roleSlug: req.params.roleSlug + selector: { + slug: req.params.roleSlug + } }); - return { role }; + + return { role: { ...role, projectId: role.projectId as string } }; } }); }; diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index a6d4459e4..f8ac34b4b 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; -import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ApiDocsTags, IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; @@ -11,7 +11,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchema/identitiy-additional-privilege"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { server.route({ @@ -43,7 +43,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F z.object({ isTemporary: z.literal(true), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.temporaryMode), temporaryRange: z .string() @@ -64,18 +64,31 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.create({ - actorAuthMethod: req.permission.authMethod, - actorId: req.permission.id, - actorOrgId: req.permission.orgId, - actor: req.permission.type, - projectId: req.body.projectId, - identityId: req.body.identityId, - ...req.body.type, - slug: req.body.slug || slugify(alphaNumericNanoId(8)), - permissions: req.body.permissions + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.createAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: req.body.projectId, + orgId: req.permission.orgId + }, + data: { + actorId: req.body.identityId, + actorType: ActorType.IDENTITY, + ...req.body.type, + name: req.body.slug || slugify(alphaNumericNanoId(8)), + permissions: req.body.permissions + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: req.body.identityId, + projectMembershipId: req.body.projectId, + projectId: req.body.projectId, + slug: privilege.name + } + }; } }); @@ -108,7 +121,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F z.object({ isTemporary: z.literal(true).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary), temporaryMode: z - .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .nativeEnum(TemporaryPermissionMode) .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.temporaryMode), temporaryRange: z .string() @@ -129,19 +142,36 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.updateById({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - actorAuthMethod: req.permission.authMethod, - id: req.params.id, + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.id); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.updateAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: privilegeDoc.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.id, + actorId: privilegeDoc.actorIdentityId as string, + actorType: ActorType.IDENTITY + }, data: { ...req.body, ...req.body.type, permissions: req.body.permissions || undefined } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: privilegeDoc.actorIdentityId as string, + projectMembershipId: privilegeDoc.projectId as string, + projectId: privilegeDoc.projectId as string, + slug: privilege.name + } + }; } }); @@ -171,14 +201,31 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.deleteById({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - id: req.params.id + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.id); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.deleteAdditionalPrivilege({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: privilegeDoc.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.id, + actorId: privilegeDoc.actorIdentityId as string, + actorType: ActorType.IDENTITY + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: privilegeDoc.actorIdentityId as string, + projectMembershipId: privilegeDoc.projectId as string, + projectId: privilegeDoc.projectId as string, + slug: privilege.name + } + }; } }); @@ -208,14 +255,31 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsById({ - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - id: req.params.id + const { privilege: privilegeDoc } = await server.services.convertor.additionalPrivilegeIdToDoc(req.params.id); + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeById({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: privilegeDoc.projectId as string, + orgId: req.permission.orgId + }, + selector: { + id: req.params.id, + actorId: privilegeDoc.actorIdentityId as string, + actorType: ActorType.IDENTITY + } }); - return { privilege }; + + return { + privilege: { + ...privilege, + identityId: privilegeDoc.actorIdentityId as string, + projectMembershipId: privilegeDoc.projectId as string, + projectId: privilegeDoc.projectId as string, + slug: privilege.name + } + }; } }); @@ -249,15 +313,36 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilegeV2.getPrivilegeDetailsBySlug({ - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - slug: req.params.privilegeSlug, - ...req.query + const { id: projectId } = await server.services.convertor.projectSlugToId({ + slug: req.query.projectSlug, + orgId: req.permission.orgId }); - return { privilege }; + + const { additionalPrivilege: privilege } = await server.services.additionalPrivilege.getAdditionalPrivilegeByName( + { + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId, + orgId: req.permission.orgId + }, + selector: { + name: req.params.privilegeSlug, + actorId: req.query.identityId, + actorType: ActorType.IDENTITY + } + } + ); + + return { + privilege: { + ...privilege, + identityId: req.query.identityId, + projectMembershipId: privilege.projectId as string, + projectId, + slug: privilege.name + } + }; } }); @@ -288,15 +373,27 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privileges = await server.services.identityProjectAdditionalPrivilegeV2.listIdentityProjectPrivileges({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.query + const { additionalPrivileges: privileges } = await server.services.additionalPrivilege.listAdditionalPrivileges({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + projectId: req.query.projectId, + orgId: req.permission.orgId + }, + selector: { + actorId: req.query.identityId, + actorType: ActorType.IDENTITY + } }); + return { - privileges + privileges: privileges.map((privilege) => ({ + ...privilege, + identityId: req.query.identityId, + projectMembershipId: privilege.projectId as string, + projectId: req.query.projectId, + slug: privilege.name + })) }; } }); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 95d1a9877..16c7e962c 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -1,21 +1,20 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; +import { TMembershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-request/access-approval-request-reviewer-dal"; import { ApprovalStatus } from "../access-approval-request/access-approval-request-types"; import { TGroupDALFactory } from "../group/group-dal"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TAccessApprovalPolicyApproverDALFactory, TAccessApprovalPolicyBypasserDALFactory @@ -39,14 +38,13 @@ type TAccessApprovalPolicyServiceFactoryDep = { projectEnvDAL: Pick; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; accessApprovalPolicyBypasserDAL: TAccessApprovalPolicyBypasserDALFactory; - projectMembershipDAL: Pick; groupDAL: TGroupDALFactory; userDAL: Pick; accessApprovalRequestDAL: Pick; - additionalPrivilegeDAL: Pick; + additionalPrivilegeDAL: Pick; accessApprovalRequestReviewerDAL: Pick; - orgMembershipDAL: Pick; accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory; + membershipUserDAL: TMembershipUserDALFactory; }; export const accessApprovalPolicyServiceFactory = ({ @@ -62,7 +60,7 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalRequestDAL, additionalPrivilegeDAL, accessApprovalRequestReviewerDAL, - orgMembershipDAL + membershipUserDAL }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { const $policyExists = async ({ envId, @@ -424,13 +422,14 @@ export const accessApprovalPolicyServiceFactory = ({ // Validate user bypassers if (bypasserUserIds.length > 0) { - const orgMemberships = await orgMembershipDAL.find({ - $in: { userId: bypasserUserIds }, - orgId: actorOrgId + const orgMemberships = await membershipUserDAL.find({ + $in: { actorUserId: bypasserUserIds }, + scopeOrgId: actorOrgId, + scope: AccessScope.Organization }); if (orgMemberships.length !== bypasserUserIds.length) { - const foundUserIdsInOrg = new Set(orgMemberships.map((mem) => mem.userId)); + const foundUserIdsInOrg = new Set(orgMemberships.map((mem) => mem.actorUserId as string)); const missingUserIds = bypasserUserIds.filter((id) => !foundUserIdsInOrg.has(id)); throw new BadRequestError({ message: `One or more specified bypasser users are not part of the organization or do not exist. Invalid or non-member user IDs: ${missingUserIds.join(", ")}` @@ -633,7 +632,7 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -641,9 +640,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index f3972fc1c..3bea66696 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -3,9 +3,10 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { AccessApprovalRequestsSchema, + AccessScope, TableName, TAccessApprovalRequests, - TOrgMemberships, + TMemberships, TUserGroupMembership, TUsers } from "@app/db/schemas"; @@ -244,11 +245,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR const docs = await db .replicaNode()(TableName.AccessApprovalRequest) .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) - .leftJoin( - TableName.ProjectUserAdditionalPrivilege, + TableName.AdditionalPrivilege, `${TableName.AccessApprovalRequest}.privilegeId`, - `${TableName.ProjectUserAdditionalPrivilege}.id` + `${TableName.AdditionalPrivilege}.id` ) .leftJoin( TableName.AccessApprovalPolicy, @@ -276,7 +276,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR `${TableName.UserGroupMembership}.groupId` ) .leftJoin(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .leftJoin( TableName.AccessApprovalPolicyBypasser, `${TableName.AccessApprovalPolicy}.id`, @@ -294,24 +293,24 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR `requestedByUser.id` ) - .leftJoin( - db(TableName.OrgMembership).as("approverOrgMembership"), - `${TableName.AccessApprovalPolicyApprover}.approverUserId`, - `approverOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("approverGroupOrgMembership"), - `${TableName.Users}.id`, - `approverGroupOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("reviewerOrgMembership"), - `${TableName.AccessApprovalRequestReviewer}.reviewerUserId`, - `reviewerOrgMembership.userId` - ) - + .leftJoin(db(TableName.Membership).as("approverOrgMembership"), (qb) => { + qb.on( + `${TableName.AccessApprovalPolicyApprover}.approverUserId`, + `approverOrgMembership.actorUserId` + ).andOn(`approverOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) + .leftJoin(db(TableName.Membership).as("approverGroupOrgMembership"), (qb) => { + qb.on(`${TableName.Users}.id`, `approverGroupOrgMembership.actorUserId`).andOn( + `approverGroupOrgMembership.scope`, + db.raw("?", [AccessScope.Organization]) + ); + }) + .leftJoin(db(TableName.Membership).as("reviewerOrgMembership"), (qb) => { + qb.on( + `${TableName.AccessApprovalRequestReviewer}.reviewerUserId`, + `reviewerOrgMembership.actorUserId` + ).andOn(`reviewerOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .select(selectAllTableCols(TableName.AccessApprovalRequest)) @@ -360,22 +359,22 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"), db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"), - db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"), + db.ref("actorUserId").withSchema(TableName.AdditionalPrivilege).as("privilegeUserId"), + db.ref("projectId").withSchema(TableName.AdditionalPrivilege).as("privilegeMembershipId"), - db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), - db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("privilegeIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("privilegeTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("privilegeTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("privilegeTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("privilegeTemporaryAccessEndTime"), - db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("privilegePermissions") ) .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc"); @@ -408,7 +407,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR privilege: doc.privilegeId ? { membershipId: doc.privilegeMembershipId, - userId: doc.privilegeUserId, + userId: doc.privilegeUserId || "", projectId: doc.projectId, isTemporary: doc.privilegeIsTemporary, temporaryMode: doc.privilegeTemporaryMode, @@ -773,9 +772,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR ) .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) .leftJoin( - TableName.ProjectUserAdditionalPrivilege, + TableName.AdditionalPrivilege, `${TableName.AccessApprovalRequest}.privilegeId`, - `${TableName.ProjectUserAdditionalPrivilege}.id` + `${TableName.AdditionalPrivilege}.id` ) .leftJoin( 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 d69c6da79..1027995a7 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 @@ -1,7 +1,7 @@ import slugify from "@sindresorhus/slugify"; import msFn from "ms"; -import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, TemporaryPermissionMode } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -10,12 +10,12 @@ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { EnforcementLevel } from "@app/lib/types"; import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; import { TriggerFeature } from "@app/lib/workflow-integrations/types"; +import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -26,15 +26,13 @@ import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-poli import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TGroupDALFactory } from "../group/group-dal"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; -import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { verifyRequestedPermissions } from "./access-approval-request-fns"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-approval-request-types"; type TSecretApprovalRequestServiceFactoryDep = { - additionalPrivilegeDAL: Pick; + additionalPrivilegeDAL: Pick; permissionService: Pick; accessApprovalPolicyApproverDAL: Pick; projectEnvDAL: Pick; @@ -59,7 +57,6 @@ type TSecretApprovalRequestServiceFactoryDep = { "create" | "find" | "findOne" | "transaction" | "delete" >; groupDAL: Pick; - projectMembershipDAL: Pick; smtpService: Pick; userDAL: Pick< TUserDALFactory, @@ -125,7 +122,7 @@ export const accessApprovalRequestServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); // Anyone can create an access approval request. - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -133,9 +130,6 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const requestedByUser = await userDAL.findById(actorId); if (!requestedByUser) throw new ForbiddenRequestError({ message: "User not found" }); @@ -340,7 +334,7 @@ export const accessApprovalRequestServiceFactory = ({ }); } - const { membership, hasRole } = await permissionService.getProjectPermission({ + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId: accessApprovalRequest.projectId, @@ -349,10 +343,6 @@ export const accessApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } - const isApprover = policy.approvers.find((approver) => approver.userId === actorId); if (!hasRole(ProjectMembershipRole.Admin) && !isApprover) { @@ -496,7 +486,7 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -504,9 +494,6 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); @@ -566,7 +553,7 @@ export const accessApprovalRequestServiceFactory = ({ slug: permissionEnvironment }); - const { membership, hasRole } = await permissionService.getProjectPermission({ + const { hasRole } = await permissionService.getProjectPermission({ actor, actorId, projectId: accessApprovalRequest.projectId, @@ -575,10 +562,6 @@ export const accessApprovalRequestServiceFactory = ({ actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } - const isSelfApproval = actorId === accessApprovalRequest.requestedByUserId; const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft; const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId); @@ -724,9 +707,9 @@ export const accessApprovalRequestServiceFactory = ({ // Permanent access const privilege = await additionalPrivilegeDAL.create( { - userId: accessApprovalRequest.requestedByUserId, + actorUserId: accessApprovalRequest.requestedByUserId, projectId: accessApprovalRequest.projectId, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + name: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions) }, tx @@ -739,12 +722,12 @@ export const accessApprovalRequestServiceFactory = ({ const privilege = await additionalPrivilegeDAL.create( { - userId: accessApprovalRequest.requestedByUserId, + actorUserId: accessApprovalRequest.requestedByUserId, projectId: accessApprovalRequest.projectId, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + name: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions), isTemporary: true, // Explicitly set to true for the privilege - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryMode: TemporaryPermissionMode.Relative, temporaryRange: accessApprovalRequest.temporaryRange!, temporaryAccessStartTime: startTime, temporaryAccessEndTime: new Date(startTime.getTime() + relativeTempAllocatedTimeInMs) @@ -830,7 +813,7 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId: project.id, @@ -838,9 +821,6 @@ export const accessApprovalRequestServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } const count = await accessApprovalRequestDAL.getCount({ projectId: project.id, policyId }); diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts index da9d3c1ef..e0e7e582e 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts @@ -16,11 +16,6 @@ export const gatewayV2DalFactory = (db: TDbClient) => { // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.GatewayV2)) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.GatewayV2}.identityId`) - .join( - TableName.IdentityOrgMembership, - `${TableName.IdentityOrgMembership}.identityId`, - `${TableName.GatewayV2}.identityId` - ) .select(selectAllTableCols(TableName.GatewayV2)) .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts index c21ff31c0..58f59d313 100644 --- a/backend/src/ee/services/gateway/gateway-dal.ts +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -1,5 +1,5 @@ import { TDbClient } from "@app/db"; -import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; +import { AccessScope, GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; @@ -17,17 +17,14 @@ export const gatewayDALFactory = (db: TDbClient) => { // eslint-disable-next-line @typescript-eslint/no-misused-promises .where(buildFindFilter(filter, TableName.Gateway, ["orgId"])) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) - .join( - TableName.IdentityOrgMembership, - `${TableName.IdentityOrgMembership}.identityId`, - `${TableName.Gateway}.identityId` - ) + .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Gateway}.identityId`) .select(selectAllTableCols(TableName.Gateway)) - .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId")) - .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); + .select(db.ref("scopeOrgId").withSchema(TableName.Membership).as("identityOrgId")) + .select(db.ref("name").withSchema(TableName.Identity).as("identityName")) + .where(`${TableName.Membership}.scope`, AccessScope.Organization); if (filter.orgId) { - void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId); + void query.where(`${TableName.Membership}.scopeOrgId`, filter.orgId); } if (limit) void query.limit(limit); if (offset) void query.offset(offset); @@ -39,7 +36,7 @@ export const gatewayDALFactory = (db: TDbClient) => { return docs.map((el) => ({ ...GatewaysSchema.parse(el), - orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string + orgId: el.identityOrgId, identity: { id: el.identityId, name: el.identityName } })); } catch (error) { diff --git a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts index 7c4ad15eb..b2bcb4ef3 100644 --- a/backend/src/ee/services/github-org-sync/github-org-sync-service.ts +++ b/backend/src/ee/services/github-org-sync/github-org-sync-service.ts @@ -6,13 +6,15 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql"; import { Octokit as OctokitRest } from "@octokit/rest"; import RE2 from "re2"; -import { OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { retryWithBackoff } from "@app/lib/retry"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TGroupDALFactory } from "../group/group-dal"; @@ -77,11 +79,10 @@ type TGithubOrgSyncServiceFactoryDep = { "findGroupMembershipsByUserIdInOrg" | "findGroupMembershipsByGroupIdInOrg" | "insertMany" | "delete" >; groupDAL: Pick; + membershipRoleDAL: Pick; + membershipGroupDAL: Pick; licenseService: Pick; - orgMembershipDAL: Pick< - TOrgMembershipDALFactory, - "find" | "findOrgMembershipById" | "findOrgMembershipsWithUsersByOrgId" - >; + orgMembershipDAL: Pick; }; export type TGithubOrgSyncServiceFactory = ReturnType; @@ -93,7 +94,9 @@ export const githubOrgSyncServiceFactory = ({ userGroupMembershipDAL, groupDAL, licenseService, - orgMembershipDAL + orgMembershipDAL, + membershipRoleDAL, + membershipGroupDAL }: TGithubOrgSyncServiceFactoryDep) => { const createGithubOrgSync = async ({ githubOrgName, @@ -368,6 +371,23 @@ export const githubOrgSyncServiceFactory = ({ })), tx ); + const memberships = await membershipGroupDAL.insertMany( + newGroups.map((el) => ({ + actorGroupId: el.id, + scope: AccessScope.Organization, + scopeOrgId: orgId + })), + tx + ); + + await membershipRoleDAL.insertMany( + memberships.map((el) => ({ + membershipId: el.id, + role: OrgMembershipRole.Member + })), + tx + ); + await userGroupMembershipDAL.insertMany( newGroups.map((el) => ({ groupId: el.id, @@ -694,6 +714,23 @@ export const githubOrgSyncServiceFactory = ({ tx ); + const memberships = await membershipGroupDAL.insertMany( + newGroups.map((el) => ({ + actorGroupId: el.id, + scope: AccessScope.Organization, + scopeOrgId: orgPermission.orgId + })), + tx + ); + + await membershipRoleDAL.insertMany( + memberships.map((el) => ({ + membershipId: el.id, + role: OrgMembershipRole.Member + })), + tx + ); + newGroups.forEach((group) => { if (!existingTeamsMap[group.name]) { existingTeamsMap[group.name] = []; diff --git a/backend/src/ee/services/group/group-dal.ts b/backend/src/ee/services/group/group-dal.ts index 708fbdbd3..90d499669 100644 --- a/backend/src/ee/services/group/group-dal.ts +++ b/backend/src/ee/services/group/group-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TGroups } from "@app/db/schemas"; +import { AccessScope, TableName, TGroups } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; @@ -36,14 +36,21 @@ export const groupDALFactory = (db: TDbClient) => { try { const docs = await (tx || db.replicaNode())(TableName.Groups) .where(`${TableName.Groups}.orgId`, orgId) - .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .join(TableName.Membership, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select(selectAllTableCols(TableName.Groups)) // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)); + .select(db.ref("id").as("crId").withSchema(TableName.Role)) + .select(db.ref("name").as("crName").withSchema(TableName.Role)) + .select(db.ref("role").withSchema(TableName.MembershipRole)) + .select(db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.Role)) + .select(db.ref("description").as("crDescription").withSchema(TableName.Role)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.Role)); + return docs.map(({ crId, crDescription, crSlug, crPermission, crName, ...el }) => ({ ...el, customRole: el.roleId @@ -81,9 +88,11 @@ export const groupDALFactory = (db: TDbClient) => { }) => { try { const query = db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .leftJoin(TableName.UserGroupMembership, (bd) => { bd.on(`${TableName.UserGroupMembership}.userId`, "=", `${TableName.Users}.id`).andOn( `${TableName.UserGroupMembership}.groupId`, @@ -92,7 +101,7 @@ export const groupDALFactory = (db: TDbClient) => { ); }) .select( - db.ref("id").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("createdAt").withSchema(TableName.UserGroupMembership).as("joinedGroupAt"), db.ref("email").withSchema(TableName.Users), @@ -160,8 +169,10 @@ export const groupDALFactory = (db: TDbClient) => { const findGroupsByProjectId = async (projectId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.Groups) - .join(TableName.GroupProjectMembership, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorGroupId`) .select(selectAllTableCols(TableName.Groups)); return docs; } catch (error) { @@ -172,11 +183,16 @@ export const groupDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { const doc = await (tx || db.replicaNode())(TableName.Groups) - .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .where(`${TableName.Groups}.id`, id) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) .select( selectAllTableCols(TableName.Groups), - db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles) + db.ref("slug").as("customRoleSlug").withSchema(TableName.Role), + db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole), + db.ref("role").withSchema(TableName.MembershipRole) ) .first(); @@ -186,12 +202,36 @@ export const groupDALFactory = (db: TDbClient) => { } }; + const findOne = async (filter: Partial, tx?: Knex): Promise => { + try { + const doc = await (tx || db.replicaNode())(TableName.Groups) + .join(TableName.Membership, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where((queryBuilder) => { + Object.entries(filter).forEach(([key, value]) => { + void queryBuilder.where(`${TableName.Groups}.${key}`, value); + }); + }) + .select( + selectAllTableCols(TableName.Groups), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").as("roleId").withSchema(TableName.MembershipRole) + ) + .first(); + return doc; + } catch (error) { + throw new DatabaseError({ error, name: "Find one" }); + } + }; + return { ...groupOrm, findGroups, findByOrgId, findAllGroupPossibleMembers, findGroupsByProjectId, - findById + findById, + findOne }; }; diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 56f8df6c0..c4384cc4e 100644 --- a/backend/src/ee/services/group/group-fns.ts +++ b/backend/src/ee/services/group/group-fns.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; @@ -16,7 +16,7 @@ const addAcceptedUsersToGroup = async ({ group, userGroupMembershipDAL, userDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -42,13 +42,15 @@ const addAcceptedUsersToGroup = async ({ const projectIds = Array.from( new Set( ( - await groupProjectDAL.find( + await membershipGroupDAL.find( { - groupId: group.id + actorGroupId: group.id, + scopeOrgId: group.orgId, + scope: AccessScope.Project }, { tx } ) - ).map((gp) => gp.projectId) + ).map((gp) => gp.scopeProjectId as string) ) ); @@ -167,11 +169,11 @@ export const addUsersToGroupByUserIds = async ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TAddUsersToGroupByUserIds) => { const processAddition = async (tx: Knex) => { const foundMembers = await userDAL.find( @@ -214,15 +216,18 @@ export const addUsersToGroupByUserIds = async ({ // check if all user(s) are part of the organization const existingUserOrgMemberships = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.orgId` as "orgId"]: group.orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: group.orgId, + scope: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: userIds + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userIds } }, { tx } ); - const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId)); + const existingUserOrgMembershipsUserIdsSet = new Set( + existingUserOrgMemberships.map((u) => u.actorUserId as string) + ); userIds.forEach((userId) => { if (!existingUserOrgMembershipsUserIdsSet.has(userId)) @@ -250,7 +255,7 @@ export const addUsersToGroupByUserIds = async ({ group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -292,9 +297,9 @@ export const removeUsersFromGroupByUserIds = async ({ userIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TRemoveUsersFromGroupByUserIds) => { const processRemoval = async (tx: Knex) => { const foundMembers = await userDAL.find({ @@ -352,13 +357,15 @@ export const removeUsersFromGroupByUserIds = async ({ const projectIds = Array.from( new Set( ( - await groupProjectDAL.find( + await membershipGroupDAL.find( { - groupId: group.id + scope: AccessScope.Project, + actorGroupId: group.id, + scopeOrgId: group.orgId }, { tx } ) - ).map((gp) => gp.projectId) + ).map((gp) => gp.scopeProjectId as string) ) ); @@ -422,11 +429,11 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({ userIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, - tx: outerTx + tx: outerTx, + membershipGroupDAL }: TConvertPendingGroupAdditionsToGroupMemberships) => { const processConversion = async (tx: Knex) => { const users = await userDAL.find( @@ -463,7 +470,7 @@ export const convertPendingGroupAdditionsToGroupMemberships = async ({ group: pendingGroupAddition.group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index d4d30b5ae..075488488 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,11 +1,12 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -35,8 +36,9 @@ type TGroupServiceFactoryDep = { TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" >; - groupProjectDAL: Pick; - orgDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; + orgDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" @@ -46,7 +48,7 @@ type TGroupServiceFactoryDep = { projectKeyDAL: Pick; permissionService: Pick< TPermissionServiceFactory, - "getOrgPermission" | "getOrgPermissionByRole" | "invalidateProjectPermissionCache" + "getOrgPermission" | "getOrgPermissionByRoles" | "invalidateProjectPermissionCache" >; licenseService: Pick; oidcConfigDAL: Pick; @@ -57,7 +59,6 @@ export type TGroupServiceFactory = ReturnType; export const groupServiceFactory = ({ userDAL, groupDAL, - groupProjectDAL, orgDAL, userGroupMembershipDAL, projectDAL, @@ -65,12 +66,14 @@ export const groupServiceFactory = ({ projectKeyDAL, permissionService, licenseService, - oidcConfigDAL + oidcConfigDAL, + membershipGroupDAL, + membershipRoleDAL }: TGroupServiceFactoryDep) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -85,25 +88,23 @@ export const groupServiceFactory = ({ message: "Failed to create group due to plan restriction. Upgrade plan to create group." }); - const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - role, - actorOrgId - ); - const isCustomRole = Boolean(customRole); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); if (role !== OrgMembershipRole.NoAccess) { const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), @@ -125,7 +126,25 @@ export const groupServiceFactory = ({ slug: slug || slugify(`${name}-${alphaNumericNanoId(4)}`), orgId: actorOrgId, role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + roleId: null + }, + tx + ); + + const membership = await membershipGroupDAL.create( + { + actorGroupId: newGroup.id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: isCustomRole ? OrgMembershipRole.Custom : role, + customRoleId: rolePermissionDetails?.role?.id }, tx ); @@ -148,7 +167,7 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -169,32 +188,31 @@ export const groupServiceFactory = ({ throw new NotFoundError({ message: `Failed to find group with ID ${id}` }); } - let customRole: TOrgRoles | undefined; + let customRole: TRoles | undefined; if (role) { - const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( - role, - group.orgId - ); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], group.orgId); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); - const isCustomRole = Boolean(customOrgRole); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } }); - if (isCustomRole) customRole = customOrgRole; + if (isCustomRole) customRole = rolePermissionDetails?.role; } const updatedGroup = await groupDAL.transaction(async (tx) => { @@ -208,35 +226,44 @@ export const groupServiceFactory = ({ } } - const [updated] = await groupDAL.update( - { - id: group.id - }, - { - name, - slug: slug ? slugify(slug) : undefined, - ...(role - ? { - role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id ?? null - } - : {}) - }, - tx - ); + let updated = group; + + if (name || slug) { + [updated] = await groupDAL.update( + { + id: group.id + }, + { + name, + slug: slug ? slugify(slug) : undefined + }, + tx + ); + } + + if (role) { + const membership = await membershipGroupDAL.findOne( + { + scope: AccessScope.Organization, + actorGroupId: updated.id, + scopeOrgId: updated.orgId + }, + tx + ); + await membershipRoleDAL.delete({ membershipId: membership.id }, tx); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: customRole ? OrgMembershipRole.Custom : role, + customRoleId: customRole?.id ?? null + }, + tx + ); + } return updated; }); - if (role) { - const groupProjects = await groupProjectDAL.find({ groupId: group.id }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => - permissionService.invalidateProjectPermissionCache(groupProject.projectId) - ) - ]); - } - return updatedGroup; }; @@ -259,17 +286,11 @@ export const groupServiceFactory = ({ message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." }); - const groupProjects = await groupProjectDAL.find({ groupId: id }); - const [group] = await groupDAL.delete({ id, orgId: actorOrgId }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) - ]); - return group; }; @@ -344,7 +365,7 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -376,22 +397,23 @@ export const groupServiceFactory = ({ }); } - const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups, permission, - groupRolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to add user to more privileged group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups ), @@ -410,17 +432,12 @@ export const groupServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, projectDAL, projectBotDAL }); - const groupProjects = await groupProjectDAL.find({ groupId: group.id }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) - ]); - return users[0]; }; @@ -434,7 +451,7 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -466,21 +483,22 @@ export const groupServiceFactory = ({ }); } - const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([group.role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups, permission, - groupRolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to delete user from more privileged group", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups ), @@ -498,15 +516,10 @@ export const groupServiceFactory = ({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL }); - const groupProjects = await groupProjectDAL.find({ groupId: group.id }); - await Promise.allSettled([ - ...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId)) - ]); - return users[0]; }; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index e91f7a47b..4b0742201 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -3,7 +3,7 @@ import { Knex } from "knex"; import { TGroups } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TGenericPermission } from "@app/lib/types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; @@ -63,7 +63,7 @@ export type TAddUsersToGroup = { group: TGroups; userDAL: Pick; userGroupMembershipDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -76,7 +76,7 @@ export type TAddUsersToGroupByUserIds = { userDAL: Pick; userGroupMembershipDAL: Pick; orgDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -88,7 +88,7 @@ export type TRemoveUsersFromGroupByUserIds = { userIds: string[]; userDAL: Pick; userGroupMembershipDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; tx?: Knex; }; @@ -100,7 +100,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" >; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; diff --git a/backend/src/ee/services/group/user-group-membership-dal.ts b/backend/src/ee/services/group/user-group-membership-dal.ts index 374459b0c..886eebe29 100644 --- a/backend/src/ee/services/group/user-group-membership-dal.ts +++ b/backend/src/ee/services/group/user-group-membership-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; @@ -18,21 +18,19 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { */ const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[], tx?: Knex) => { try { - const userProjectMemberships: string[] = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where(`${TableName.ProjectMembership}.userId`, userId) - .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) - .pluck(`${TableName.ProjectMembership}.projectId`); + const userProjectMemberships: string[] = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.actorUserId`, userId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereIn(`${TableName.Membership}.scopeProjectId`, projectIds) + .pluck(`${TableName.Membership}.scopeProjectId`); const userGroupMemberships: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId) - .join( - TableName.GroupProjectMembership, - `${TableName.UserGroupMembership}.groupId`, - `${TableName.GroupProjectMembership}.groupId` - ) - .whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds) - .pluck(`${TableName.GroupProjectMembership}.projectId`); + .join(TableName.Membership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereIn(`${TableName.Membership}.scopeProjectId`, projectIds) + .pluck(`${TableName.Membership}.scopeProjectId`); return new Set(userProjectMemberships.concat(userGroupMemberships)); } catch (error) { @@ -44,13 +42,10 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => { try { const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership) - .join( - TableName.GroupProjectMembership, - `${TableName.UserGroupMembership}.groupId`, - `${TableName.GroupProjectMembership}.groupId` - ) + .join(TableName.Membership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + .where(`${TableName.Membership}.scopeProjectId`, projectId) .whereIn(`${TableName.Users}.username`, usernames) .pluck(`${TableName.Users}.id`); @@ -73,24 +68,25 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { try { // get list of groups in the project with id [projectId] // that that are not the group with id [groupId] - const groups: string[] = await (tx || db.replicaNode())(TableName.GroupProjectMembership) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) - .whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId) - .pluck(`${TableName.GroupProjectMembership}.groupId`); + const groups: string[] = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .whereNot(`${TableName.Membership}.actorGroupId`, groupId) + .pluck(`${TableName.Membership}.actorGroupId`); // main query const members = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.groupId`, groupId) .where(`${TableName.UserGroupMembership}.isPending`, false) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.ProjectMembership, (bd) => { - bd.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn( - `${TableName.ProjectMembership}.projectId`, + .leftJoin(TableName.Membership, (bd) => { + bd.on(`${TableName.Users}.id`, "=", `${TableName.Membership}.actorUserId`).andOn( + `${TableName.Membership}.scopeProjectId`, "=", db.raw("?", [projectId]) ); }) - .whereNull(`${TableName.ProjectMembership}.userId`) + .whereNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, @@ -166,15 +162,17 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { const docs = await db .replicaNode()(TableName.UserGroupMembership) .join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) - .join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`) + .join(TableName.Membership, `${TableName.UserGroupMembership}.userId`, `${TableName.Membership}.actorUserId`) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.UserGroupMembership}.userId`, userId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .where(`${TableName.Groups}.orgId`, orgId) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("name").withSchema(TableName.Groups).as("groupName"), - db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"), + db.ref("id").withSchema(TableName.Membership).as("orgMembershipId"), db.ref("firstName").withSchema(TableName.Users).as("firstName"), db.ref("lastName").withSchema(TableName.Users).as("lastName"), db.ref("slug").withSchema(TableName.Groups).as("groupSlug") @@ -191,15 +189,17 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => { const docs = await db .replicaNode()(TableName.UserGroupMembership) .join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) - .join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`) + .join(TableName.Membership, `${TableName.UserGroupMembership}.userId`, `${TableName.Membership}.actorUserId`) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.Groups}.id`, groupId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .where(`${TableName.Groups}.orgId`, orgId) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("name").withSchema(TableName.Groups).as("groupName"), - db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"), + db.ref("id").withSchema(TableName.Membership).as("orgMembershipId"), db.ref("firstName").withSchema(TableName.Users).as("firstName"), db.ref("lastName").withSchema(TableName.Users).as("lastName") ); diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts deleted file mode 100644 index a7d8794a4..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectAdditionalPrivilegeV2DALFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeV2DALFactory ->; - -export const identityProjectAdditionalPrivilegeV2DALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts deleted file mode 100644 index 27b67367e..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { ForbiddenError, subject } from "@casl/ability"; -import { packRules } from "@casl/ability/extra"; - -import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; - -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; -import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; -import { - IdentityProjectAdditionalPrivilegeTemporaryMode, - TCreateIdentityPrivilegeDTO, - TDeleteIdentityPrivilegeByIdDTO, - TGetIdentityPrivilegeDetailsByIdDTO, - TGetIdentityPrivilegeDetailsBySlugDTO, - TListIdentityPrivilegesDTO, - TUpdateIdentityPrivilegeByIdDTO -} from "./identity-project-additional-privilege-v2-types"; - -type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = { - identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; - identityProjectDAL: Pick; - projectDAL: Pick; - permissionService: Pick; -}; - -export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeV2ServiceFactory ->; - -export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ - identityProjectAdditionalPrivilegeDAL, - identityProjectDAL, - projectDAL, - permissionService -}: TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep) => { - const create = async ({ - slug, - actor, - actorId, - projectId, - actorOrgId, - identityId, - permissions: customPermission, - actorAuthMethod, - ...dto - }: TCreateIdentityPrivilegeDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug) throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateById = async ({ - id, - data, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TUpdateIdentityPrivilegeByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - if (data?.slug) { - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: data.slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug && existingSlug.id !== identityPrivilege.id) - throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); - } - - const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; - if (isTemporary) { - const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: data.isTemporary, - temporaryRange: data.temporaryRange, - temporaryMode: data.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteById = async ({ actorId, id, actor, actorOrgId, actorAuthMethod }: TDeleteIdentityPrivilegeByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - identityRolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsById = async ({ - id, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsByIdDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(id); - if (!identityPrivilege) throw new NotFoundError({ message: `Identity privilege with ${id} not found` }); - - const identityProjectMembership = await identityProjectDAL.findOne({ id: identityPrivilege.projectMembershipId }); - if (!identityProjectMembership) - throw new NotFoundError({ - message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsBySlug = async ({ - identityId, - slug, - projectSlug, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsBySlugDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug ${slug} not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) throw new NotFoundError({ message: "Identity additional privilege not found" }); - - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const listIdentityProjectPrivileges = async ({ - identityId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - projectId - }: TListIdentityPrivilegesDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) - ); - - const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find( - { - projectMembershipId: identityProjectMembership.id - }, - { sort: [[`${TableName.IdentityProjectAdditionalPrivilege}.slug` as "slug", "asc"]] } - ); - return identityPrivileges; - }; - - return { - getPrivilegeDetailsById, - getPrivilegeDetailsBySlug, - listIdentityProjectPrivileges, - create, - updateById, - deleteById - }; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts deleted file mode 100644 index aab6b8510..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum IdentityProjectAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateIdentityPrivilegeDTO = { - permissions: TProjectPermissionV2Schema[]; - identityId: string; - projectId: string; - slug: string; -} & ( - | { - isTemporary: false; - } - | { - isTemporary: true; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit & { - data: Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - }; - -export type TDeleteIdentityPrivilegeByIdDTO = Omit & { - id: string; -}; - -export type TGetIdentityPrivilegeDetailsByIdDTO = Omit & { - id: string; -}; - -export type TListIdentityPrivilegesDTO = Omit & { - identityId: string; - projectId: string; -}; - -export type TGetIdentityPrivilegeDetailsBySlugDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts deleted file mode 100644 index 26252f2d1..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeDALFactory ->; - -export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts deleted file mode 100644 index ddba76920..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; - -import { ActionProjectType } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; - -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { - ProjectPermissionIdentityActions, - ProjectPermissionSet, - ProjectPermissionSub -} from "../permission/project-permission"; -import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; -import { - IdentityProjectAdditionalPrivilegeTemporaryMode, - TCreateIdentityPrivilegeDTO, - TDeleteIdentityPrivilegeDTO, - TGetIdentityPrivilegeDetailsDTO, - TListIdentityPrivilegesDTO, - TUpdateIdentityPrivilegeDTO -} from "./identity-project-additional-privilege-types"; - -type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { - identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; - identityProjectDAL: Pick; - projectDAL: Pick; - permissionService: Pick; -}; - -export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< - typeof identityProjectAdditionalPrivilegeServiceFactory ->; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const identityProjectAdditionalPrivilegeServiceFactory = ({ - identityProjectAdditionalPrivilegeDAL, - identityProjectDAL, - permissionService, - projectDAL -}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => { - const create = async ({ - slug, - actor, - actorId, - identityId, - projectSlug, - permissions: customPermission, - actorOrgId, - actorAuthMethod, - ...dto - }: TCreateIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); - - validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ - projectMembershipId: identityProjectMembership.id, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateBySlug = async ({ - projectSlug, - slug, - identityId, - data, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TUpdateIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - targetIdentityPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - if (data?.slug) { - const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: data.slug, - projectMembershipId: identityProjectMembership.id - }); - if (existingSlug && existingSlug.id !== identityPrivilege.id) - throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); - } - - const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; - validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; - if (isTemporary) { - const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: data.isTemporary, - temporaryRange: data.temporaryRange, - temporaryMode: data.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - slug: data.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteBySlug = async ({ - actorId, - slug, - identityId, - projectSlug, - actor, - actorOrgId, - actorAuthMethod - }: TDeleteIdentityPrivilegeDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityProjectMembership.identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - identityRolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to edit more privileged identity", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - - const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsBySlug = async ({ - projectSlug, - identityId, - slug, - actorOrgId, - actor, - actorId, - actorAuthMethod - }: TGetIdentityPrivilegeDetailsDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug, - projectMembershipId: identityProjectMembership.id - }); - if (!identityPrivilege) { - throw new NotFoundError({ - message: `Identity additional privilege with slug '${slug}' not found for the specified identity with ID '${identityProjectMembership.identityId}'` - }); - } - return { - ...identityPrivilege, - permissions: unpackPermissions(identityPrivilege.permissions) - }; - }; - - const listIdentityProjectPrivileges = async ({ - identityId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - projectSlug - }: TListIdentityPrivilegesDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - const projectId = project.id; - - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) - throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ - projectMembershipId: identityProjectMembership.id - }); - return identityPrivileges.map((el) => ({ - ...el, - permissions: unpackPermissions(el.permissions) - })); - }; - - return { - create, - updateBySlug, - deleteBySlug, - getPrivilegeDetailsBySlug, - listIdentityProjectPrivileges - }; -}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts deleted file mode 100644 index 6a0ecee5f..000000000 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum IdentityProjectAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateIdentityPrivilegeDTO = { - permissions: TProjectPermissionV2Schema[]; - identityId: string; - projectSlug: string; - slug: string; -} & ( - | { - isTemporary: false; - } - | { - isTemporary: true; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit< - TProjectPermission, - "projectId" -> & { - data: Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - }; - -export type TDeleteIdentityPrivilegeDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; - -export type TGetIdentityPrivilegeDetailsDTO = Omit & { - slug: string; - identityId: string; - projectSlug: string; -}; - -export type TListIdentityPrivilegesDTO = Omit & { - identityId: string; - projectSlug: string; -}; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 8643ecdac..43ca5ab3d 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -12,12 +12,12 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; @@ -49,13 +49,13 @@ import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal"; type TLdapConfigServiceFactoryDep = { ldapConfigDAL: Pick; ldapGroupMapDAL: Pick; - orgMembershipDAL: Pick; orgDAL: Pick< TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; groupDAL: Pick; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -87,9 +87,9 @@ export const ldapConfigServiceFactory = ({ ldapConfigDAL, ldapGroupMapDAL, orgDAL, - orgMembershipDAL, groupDAL, - groupProjectDAL, + membershipGroupDAL, + membershipRoleDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -388,25 +388,33 @@ export const ldapConfigServiceFactory = ({ await userDAL.transaction(async (tx) => { const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: userAlias.userId, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId: userAlias.userId, - orgId, - role, - roleId, + actorUserId: userAlias.userId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); } else if (orgMembership.status === OrgMembershipStatus.Invited) { await orgDAL.updateMembershipById( orgMembership.id, @@ -459,8 +467,9 @@ export const ldapConfigServiceFactory = ({ const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: newUserAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -469,16 +478,22 @@ export const ldapConfigServiceFactory = ({ await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP); const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: newUser.id, - inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + actorUserId: newUser.id, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later - isActive: true + isActive: true, + inviteEmail: email.toLowerCase() + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId }, tx ); @@ -542,10 +557,10 @@ export const ldapConfigServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); } @@ -566,7 +581,7 @@ export const ldapConfigServiceFactory = ({ userIds: [newUser.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx }); diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index cfea2573d..891d60922 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; export type TLicenseDALFactory = ReturnType; @@ -9,14 +9,14 @@ export type TLicenseDALFactory = ReturnType; export const licenseDALFactory = (db: TDbClient) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { try { - const doc = await (tx || db.replicaNode())(TableName.OrgMembership) - .where({ status: OrgMembershipStatus.Accepted }) + const doc = await (tx || db.replicaNode())(TableName.Membership) + .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); } }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); return Number(doc?.[0]?.count ?? 0); @@ -28,24 +28,27 @@ export const licenseDALFactory = (db: TDbClient) => { const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users - const userDoc = await (tx || db.replicaNode())(TableName.OrgMembership) - .where({ status: OrgMembershipStatus.Accepted }) + const userDoc = await (tx || db.replicaNode())(TableName.Membership) + .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) + .whereNotNull(`${TableName.Membership}.actorUserId`) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); } }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const identityDoc = await (tx || db.replicaNode())(TableName.Membership) + .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); } }) .count(); diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 544eeae37..c2e67908f 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -488,7 +488,7 @@ export const licenseServiceFactory = ({ const getUsageMetrics = async (orgId: string) => { const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ orgDAL.countAllOrgMembers(orgId), - identityOrgMembershipDAL.countAllOrgIdentities({ orgId }), + identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), projectDAL.countOfOrgProjects(orgId) ]); diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 445ace2b5..c2672a94e 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; -import { OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; @@ -19,12 +19,12 @@ import { OrgServiceActor } from "@app/lib/types"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; @@ -62,11 +62,12 @@ type TOidcConfigServiceFactoryDep = { TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; - orgMembershipDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; - permissionService: Pick; + permissionService: Pick; oidcConfigDAL: Pick; groupDAL: Pick; userGroupMembershipDAL: Pick< @@ -78,7 +79,6 @@ type TOidcConfigServiceFactoryDep = { | "delete" | "filterProjectsByUserMembership" >; - groupProjectDAL: Pick; projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; @@ -90,7 +90,6 @@ export type TOidcConfigServiceFactory = ReturnType { - await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId); + await permissionService.getOrgPermission(ActorType.USER, actor.id, orgId, actor.authMethod, actor.orgId); const oidcConfig = await oidcConfigDAL.findOne({ orgId, diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 0548911e8..d4da8c98f 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -219,7 +219,9 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(OrgPermissionSubjects.Groups).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGroupActions).describe( + "Describe what action an entity can take." + ) }), z.object({ subject: z.literal(OrgPermissionSubjects.SecretScanning).describe("The entity this permission pertains to."), @@ -227,11 +229,15 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(OrgPermissionSubjects.Billing).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionBillingActions).describe( + "Describe what action an entity can take." + ) }), z.object({ subject: z.literal(OrgPermissionSubjects.Identity).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionIdentityActions).describe( + "Describe what action an entity can take." + ) }), z.object({ subject: z.literal(OrgPermissionSubjects.Kms).describe("The entity this permission pertains to."), diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index cdf55127f..49a375f8f 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -1,182 +1,60 @@ +import { Knex } from "knex"; import { z } from "zod"; import { TDbClient } from "@app/db"; import { - IdentityProjectMembershipRoleSchema, - OrgMembershipRole, - OrgMembershipsSchema, + AccessScope, + AccessScopeData, + MembershipRolesSchema, + MembershipsSchema, TableName, - TIdentityOrgMemberships, - TProjectRoles, - TProjects + TMemberships, + TRoles } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { ActorType } from "@app/services/auth/auth-type"; + +interface TPermissionDataReturn extends TMemberships { + orgAuthEnforced?: boolean | null; + orgGoogleSsoAuthEnforced?: boolean | null; + shouldUseNewPrivilegeSystem?: boolean | null; + bypassOrgAuthEnabled?: boolean | null; + roles: { + id: string; + createdAt: Date; + updatedAt: Date; + isTemporary: boolean; + role: string; + temporaryRange?: string | null | undefined; + permissions?: unknown; + customRoleId?: string | null | undefined; + temporaryMode?: string | null | undefined; + temporaryAccessStartTime?: Date | null | undefined; + temporaryAccessEndTime?: Date | null | undefined; + customRoleSlug?: string | null | undefined; + }[]; + additionalPrivileges: { + id: string; + permissions: unknown; + temporaryRange: string | null | undefined; + temporaryMode: string | null | undefined; + temporaryAccessEndTime: Date | null | undefined; + temporaryAccessStartTime: Date | null | undefined; + isTemporary: boolean; + }[]; + metadata: { + id: string; + key: string; + value: string; + }[]; +} export interface TPermissionDALFactory { - getOrgPermission: ( - userId: string, + getProjectUserPermissions: ( + projectId: string, orgId: string ) => Promise< - { - status: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - role: string; - isActive: boolean; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - permissions?: unknown; - userId?: string | null | undefined; - roleId?: string | null | undefined; - inviteEmail?: string | null | undefined; - projectFavorites?: string[] | null | undefined; - customRoleSlug?: string | null | undefined; - orgAuthEnforced?: boolean | null | undefined; - orgGoogleSsoAuthEnforced: boolean; - } & { - groups: { - id: string; - updatedAt: Date; - createdAt: Date; - role: string; - roleId: string | null | undefined; - customRolePermission: unknown; - name: string; - slug: string; - orgId: string; - }[]; - } - >; - getOrgIdentityPermission: ( - identityId: string, - orgId: string - ) => Promise< - | (TIdentityOrgMemberships & { - orgAuthEnforced: boolean | null | undefined; - shouldUseNewPrivilegeSystem: boolean; - permissions?: unknown; - }) - | undefined - >; - getProjectPermission: ( - userId: string, - projectId: string - ) => Promise< - | { - roles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - additionalPrivileges: { - id: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - orgId: string; - orgAuthEnforced: boolean | null | undefined; - orgGoogleSsoAuthEnforced: boolean; - orgRole: OrgMembershipRole; - userId: string; - projectId: string; - username: string; - projectType?: string | null; - id: string; - createdAt: Date; - updatedAt: Date; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - metadata: { - id: string; - key: string; - value: string; - }[]; - userGroupRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - projecMembershiptRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - } - | undefined - >; - getProjectIdentityPermission: ( - identityId: string, - projectId: string - ) => Promise< - | { - roles: { - id: string; - createdAt: Date; - updatedAt: Date; - isTemporary: boolean; - role: string; - projectMembershipId: string; - temporaryRange?: string | null | undefined; - permissions?: unknown; - customRoleId?: string | null | undefined; - temporaryMode?: string | null | undefined; - temporaryAccessStartTime?: Date | null | undefined; - temporaryAccessEndTime?: Date | null | undefined; - customRoleSlug?: string | null | undefined; - }[]; - additionalPrivileges: { - id: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - isTemporary: boolean; - }[]; - id: string; - identityId: string; - username: string; - projectId: string; - createdAt: Date; - updatedAt: Date; - orgId: string; - projectType?: string | null; - shouldUseNewPrivilegeSystem: boolean; - orgAuthEnforced: boolean; - metadata: { - id: string; - key: string; - value: string; - }[]; - } - | undefined - >; - getProjectUserPermissions: (projectId: string) => Promise< { roles: { id: string; @@ -198,45 +76,19 @@ export interface TPermissionDALFactory { temporaryAccessEndTime: Date | null | undefined; isTemporary: boolean; }[]; - orgId: string; - orgAuthEnforced: boolean | null | undefined; userId: string; - projectId: string; username: string; - projectType?: string | null; - id: string; - createdAt: Date; - updatedAt: Date; metadata: { id: string; key: string; value: string; }[]; - userGroupRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; - projectMembershipRoles: { - id: string; - role: string; - customRoleSlug: string; - permissions: unknown; - temporaryRange: string | null | undefined; - temporaryMode: string | null | undefined; - temporaryAccessStartTime: Date | null | undefined; - temporaryAccessEndTime: Date | null | undefined; - isTemporary: boolean; - }[]; }[] >; - getProjectIdentityPermissions: (projectId: string) => Promise< + getProjectIdentityPermissions: ( + projectId: string, + orgId: string + ) => Promise< { roles: { id: string; @@ -244,7 +96,6 @@ export interface TPermissionDALFactory { updatedAt: Date; isTemporary: boolean; role: string; - projectMembershipId: string; temporaryRange?: string | null | undefined; permissions?: unknown; customRoleId?: string | null | undefined; @@ -269,8 +120,6 @@ export interface TPermissionDALFactory { createdAt: Date; updatedAt: Date; orgId: string; - projectType?: string | null; - orgAuthEnforced: boolean; metadata: { id: string; key: string; @@ -310,128 +159,203 @@ export interface TPermissionDALFactory { }[]; }[] >; + getPermission: (dto: { + scopeData: AccessScopeData; + actorId: string; + actorType: ActorType.IDENTITY | ActorType.USER; + tx?: Knex; + }) => Promise; } export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { - const getOrgPermission: TPermissionDALFactory["getOrgPermission"] = async (userId: string, orgId: string) => { + const getPermission: TPermissionDALFactory["getPermission"] = async ({ scopeData, tx, actorId, actorType }) => { try { - const groupSubQuery = db(TableName.Groups) - .where(`${TableName.Groups}.orgId`, orgId) - .join(TableName.UserGroupMembership, (queryBuilder) => { - queryBuilder - .on(`${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) - .andOn(`${TableName.UserGroupMembership}.userId`, db.raw("?", [userId])); - }) - .leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`) - .select( - db.ref("id").withSchema(TableName.Groups).as("groupId"), - db.ref("orgId").withSchema(TableName.Groups).as("groupOrgId"), - db.ref("name").withSchema(TableName.Groups).as("groupName"), - db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), - db.ref("role").withSchema(TableName.Groups).as("groupRole"), - db.ref("roleId").withSchema(TableName.Groups).as("groupRoleId"), - db.ref("createdAt").withSchema(TableName.Groups).as("groupCreatedAt"), - db.ref("updatedAt").withSchema(TableName.Groups).as("groupUpdatedAt"), - db.ref("permissions").withSchema(TableName.OrgRoles).as("groupCustomRolePermission") - ); + // akhilmhdh: when group has another group like sub group we would need recursively go down + const userGroupSubquery = (tx || db)(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, scopeData.orgId) + .where(`${TableName.UserGroupMembership}.userId`, actorId) + .select(db.ref("id").withSchema(TableName.Groups)); - const membership = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .where(`${TableName.OrgMembership}.userId`, userId) - .leftJoin(TableName.OrgRoles, `${TableName.OrgRoles}.id`, `${TableName.OrgMembership}.roleId`) - .leftJoin[0]>( - groupSubQuery.as("userGroups"), - "userGroups.groupOrgId", - db.raw("?", [orgId]) - ) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + const docs = await (tx || db) + .replicaNode()(TableName.Membership) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.AdditionalPrivilege, (qb) => { + if (actorType === ActorType.IDENTITY) { + qb.on(`${TableName.Membership}.actorIdentityId`, `${TableName.AdditionalPrivilege}.actorIdentityId`); + } else { + qb.on(`${TableName.Membership}.actorUserId`, `${TableName.AdditionalPrivilege}.actorUserId`); + } + + if (scopeData.scope === AccessScope.Organization) { + qb.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.AdditionalPrivilege}.orgId`); + } else if (scopeData.scope === AccessScope.Project) { + qb.andOn(`${TableName.Membership}.scopeProjectId`, `${TableName.AdditionalPrivilege}.projectId`); + } else { + qb.andOn(`${TableName.Membership}.scopeNamespaceId`, `${TableName.AdditionalPrivilege}.namespaceId`); + } + }) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + if (actorType === ActorType.USER) { + void queryBuilder + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + } else if (actorType === ActorType.IDENTITY) { + void queryBuilder + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + } + }) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where((qb) => { + if (actorType === ActorType.USER) { + void qb + .where(`${TableName.Membership}.actorUserId`, actorId) + .orWhereIn(`${TableName.Membership}.actorGroupId`, userGroupSubquery); + } else if (actorType === ActorType.IDENTITY) { + void qb.where(`${TableName.Membership}.actorIdentityId`, actorId); + } + }) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .select(selectAllTableCols(TableName.Membership)) .select( - selectAllTableCols(TableName.OrgMembership), + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("permissions").withSchema(TableName.Role).as("customRolePermission"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("name").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeName"), + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegePermissions"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeUpdatedAt"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), - db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), - db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), - db.ref("groupId").withSchema("userGroups"), - db.ref("groupOrgId").withSchema("userGroups"), - db.ref("groupName").withSchema("userGroups"), - db.ref("groupSlug").withSchema("userGroups"), - db.ref("groupRole").withSchema("userGroups"), - db.ref("groupRoleId").withSchema("userGroups"), - db.ref("groupCreatedAt").withSchema("userGroups"), - db.ref("groupUpdatedAt").withSchema("userGroups"), - db.ref("groupCustomRolePermission").withSchema("userGroups") + db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled") ); - const [formatedDoc] = sqlNestRelationships({ - data: membership, + const data = sqlNestRelationships({ + data: docs, key: "id", parentMapper: (el) => - OrgMembershipsSchema.extend({ - permissions: z.unknown(), + MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), + shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), - bypassOrgAuthEnabled: z.boolean(), - customRoleSlug: z.string().optional().nullable(), - shouldUseNewPrivilegeSystem: z.boolean() + bypassOrgAuthEnabled: z.boolean() }).parse(el), childrenMapper: [ { - key: "groupId", - label: "groups" as const, + key: "additionalPrivilegeId", + label: "additionalPrivileges" as const, mapper: ({ - groupId, - groupUpdatedAt, - groupCreatedAt, - groupRole, - groupRoleId, - groupCustomRolePermission, - groupName, - groupSlug, - groupOrgId + additionalPrivilegeId, + additionalPrivilegePermissions, + additionalPrivilegeIsTemporary, + additionalPrivilegeTemporaryMode, + additionalPrivilegeTemporaryRange, + additionalPrivilegeTemporaryAccessEndTime, + additionalPrivilegeTemporaryAccessStartTime, + additionalPrivilegeCreatedAt, + additionalPrivilegeUpdatedAt }) => ({ - id: groupId, - updatedAt: groupUpdatedAt, - createdAt: groupCreatedAt, - role: groupRole, - roleId: groupRoleId, - customRolePermission: groupCustomRolePermission, - name: groupName, - slug: groupSlug, - orgId: groupOrgId + id: additionalPrivilegeId, + permissions: additionalPrivilegePermissions, + temporaryRange: additionalPrivilegeTemporaryRange, + temporaryMode: additionalPrivilegeTemporaryMode, + temporaryAccessStartTime: additionalPrivilegeTemporaryAccessStartTime, + temporaryAccessEndTime: additionalPrivilegeTemporaryAccessEndTime, + isTemporary: additionalPrivilegeIsTemporary, + createdAt: additionalPrivilegeCreatedAt, + updatedAt: additionalPrivilegeUpdatedAt + }) + }, + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + customRolePermission, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + permissions: customRolePermission, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue }) } ] }); - return formatedDoc; + return data; } catch (error) { - throw new DatabaseError({ error, name: "GetOrgPermission" }); - } - }; - - const getOrgIdentityPermission: TPermissionDALFactory["getOrgIdentityPermission"] = async ( - identityId: string, - orgId: string - ) => { - try { - const membership = await db - .replicaNode()(TableName.IdentityOrgMembership) - .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) - .join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`) - .where("identityId", identityId) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) - .select(selectAllTableCols(TableName.IdentityOrgMembership)) - .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) - .select("permissions") - .select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)) - .first(); - - return membership; - } catch (error) { - throw new DatabaseError({ error, name: "GetOrgIdentityPermission" }); + throw new DatabaseError({ error, name: "Get Permission" }); } }; @@ -441,55 +365,41 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { ) => { try { const docs = await db - .replicaNode()(TableName.GroupProjectMembership) - .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - { groupCustomRoles: TableName.ProjectRoles }, - `${TableName.GroupProjectMembershipRole}.customRoleId`, + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin( + { groupCustomRoles: TableName.Role }, + `${TableName.MembershipRole}.customRoleId`, `groupCustomRoles.id` ) - .where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId) + .where(`${TableName.Membership}.scopeProjectId`, "=", projectId) .where((bd) => { if (filterGroupId) { - void bd.where(`${TableName.GroupProjectMembership}.groupId`, "=", filterGroupId); + void bd.where(`${TableName.Membership}.actorGroupId`, "=", filterGroupId); } }) .select( - db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Membership).as("membershipId"), db.ref("id").withSchema(TableName.Groups).as("groupId"), db.ref("name").withSchema(TableName.Groups).as("groupName"), db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"), db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"), - db - .ref("customRoleId") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleCustomRoleId"), - db - .ref("isTemporary") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleIsTemporary"), - db - .ref("temporaryMode") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleTemporaryMode"), - db - .ref("temporaryRange") - .withSchema(TableName.GroupProjectMembershipRole) - .as("groupProjectMembershipRoleTemporaryRange"), + db.ref("id").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("groupProjectMembershipRole"), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleCustomRoleId"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("groupProjectMembershipRoleTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.GroupProjectMembershipRole) + .withSchema(TableName.MembershipRole) .as("groupProjectMembershipRoleTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.GroupProjectMembershipRole) + .withSchema(TableName.MembershipRole) .as("groupProjectMembershipRoleTemporaryAccessEndTime") ); @@ -551,246 +461,148 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { } }; - const getProjectUserPermissions: TPermissionDALFactory["getProjectUserPermissions"] = async (projectId: string) => { + const getProjectUserPermissions: TPermissionDALFactory["getProjectUserPermissions"] = async ( + projectId: string, + orgId: string + ) => { + const userGroupSubquery = db(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, orgId) + .select(db.ref("id").withSchema(TableName.Groups)); + try { const docs = await db .replicaNode()(TableName.Users) .where("isGhost", "=", false) - .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { - void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])); + .join(TableName.Membership, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.AdditionalPrivilege, (qb) => { + qb.on(`${TableName.Membership}.actorUserId`, `${TableName.AdditionalPrivilege}.actorUserId`).andOn( + `${TableName.Membership}.scopeOrgId`, + `${TableName.AdditionalPrivilege}.orgId` + ); }) - .leftJoin( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - { groupCustomRoles: TableName.ProjectRoles }, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `groupCustomRoles.id` - ) - .join(TableName.ProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); - }) - .leftJoin( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); - }) - .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where((qb) => { + void qb + .whereNotNull(`${TableName.Membership}.actorUserId`) + .orWhereIn(`${TableName.Membership}.actorGroupId`, userGroupSubquery); + }) + .where((qb) => { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, projectId); }) .select( db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("username").withSchema(TableName.Users).as("username"), - // groups specific - db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), - db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), - db - .ref("customRoleId") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleCustomRoleId"), - db - .ref("isTemporary") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleIsTemporary"), - db - .ref("temporaryMode") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryMode"), - db - .ref("temporaryRange") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryRange"), + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("permissions").withSchema(TableName.Role).as("customRolePermission"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), - // user specific - db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), - db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryRange"), + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("name").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeName"), + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegePermissions"), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeId"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessStartTime"), + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessEndTime"), - db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), - db - .ref("permissions") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryRange"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessEndTime"), + .withSchema(TableName.AdditionalPrivilege) + .as("additionalPrivilegeTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AdditionalPrivilege).as("additionalPrivilegeUpdatedAt"), // general db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), - db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("orgId").withSchema(TableName.Project), - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("id").withSchema(TableName.Project).as("projectId") + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") ); const userPermissions = sqlNestRelationships({ data: docs, key: "userId", - parentMapper: ({ - orgId, - username, - orgAuthEnforced, - membershipId, - groupMembershipId, - membershipCreatedAt, - groupMembershipCreatedAt, - groupMembershipUpdatedAt, - membershipUpdatedAt, - projectType, - userId - }) => ({ - orgId, - orgAuthEnforced, + parentMapper: ({ username, userId }) => ({ userId, projectId, - username, - projectType, - id: membershipId || groupMembershipId, - createdAt: membershipCreatedAt || groupMembershipCreatedAt, - updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt + username }), childrenMapper: [ { - key: "userGroupProjectMembershipRoleId", - label: "userGroupRoles" as const, - mapper: ({ - userGroupProjectMembershipRoleId, - userGroupProjectMembershipRole, - userGroupProjectMembershipRolePermission, - userGroupProjectMembershipRoleCustomRoleSlug, - userGroupProjectMembershipRoleIsTemporary, - userGroupProjectMembershipRoleTemporaryMode, - userGroupProjectMembershipRoleTemporaryAccessEndTime, - userGroupProjectMembershipRoleTemporaryAccessStartTime, - userGroupProjectMembershipRoleTemporaryRange - }) => ({ - id: userGroupProjectMembershipRoleId, - role: userGroupProjectMembershipRole, - customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, - permissions: userGroupProjectMembershipRolePermission, - temporaryRange: userGroupProjectMembershipRoleTemporaryRange, - temporaryMode: userGroupProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userGroupProjectMembershipRoleIsTemporary - }) - }, - { - key: "userProjectMembershipRoleId", - label: "projectMembershipRoles" as const, - mapper: ({ - userProjectMembershipRoleId, - userProjectMembershipRole, - userProjectCustomRolePermission, - userProjectMembershipRoleIsTemporary, - userProjectMembershipRoleTemporaryMode, - userProjectMembershipRoleTemporaryRange, - userProjectMembershipRoleTemporaryAccessEndTime, - userProjectMembershipRoleTemporaryAccessStartTime, - userProjectMembershipRoleCustomRoleSlug - }) => ({ - id: userProjectMembershipRoleId, - role: userProjectMembershipRole, - customRoleSlug: userProjectMembershipRoleCustomRoleSlug, - permissions: userProjectCustomRolePermission, - temporaryRange: userProjectMembershipRoleTemporaryRange, - temporaryMode: userProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userProjectMembershipRoleIsTemporary - }) - }, - { - key: "userAdditionalPrivilegesId", + key: "additionalPrivilegeId", label: "additionalPrivileges" as const, mapper: ({ - userAdditionalPrivilegesId, - userAdditionalPrivilegesPermissions, - userAdditionalPrivilegesIsTemporary, - userAdditionalPrivilegesTemporaryMode, - userAdditionalPrivilegesTemporaryRange, - userAdditionalPrivilegesTemporaryAccessEndTime, - userAdditionalPrivilegesTemporaryAccessStartTime + additionalPrivilegeId, + additionalPrivilegePermissions, + additionalPrivilegeIsTemporary, + additionalPrivilegeTemporaryMode, + additionalPrivilegeTemporaryRange, + additionalPrivilegeTemporaryAccessEndTime, + additionalPrivilegeTemporaryAccessStartTime, + additionalPrivilegeCreatedAt, + additionalPrivilegeUpdatedAt }) => ({ - id: userAdditionalPrivilegesId, - permissions: userAdditionalPrivilegesPermissions, - temporaryRange: userAdditionalPrivilegesTemporaryRange, - temporaryMode: userAdditionalPrivilegesTemporaryMode, - temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, - temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, - isTemporary: userAdditionalPrivilegesIsTemporary + id: additionalPrivilegeId, + permissions: additionalPrivilegePermissions, + temporaryRange: additionalPrivilegeTemporaryRange, + temporaryMode: additionalPrivilegeTemporaryMode, + temporaryAccessStartTime: additionalPrivilegeTemporaryAccessStartTime, + temporaryAccessEndTime: additionalPrivilegeTemporaryAccessEndTime, + isTemporary: additionalPrivilegeIsTemporary, + createdAt: additionalPrivilegeCreatedAt, + updatedAt: additionalPrivilegeUpdatedAt + }) + }, + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + customRolePermission, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + permissions: customRolePermission, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt }) }, { @@ -808,17 +620,11 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { return userPermissions .map((userPermission) => { if (!userPermission) return undefined; - if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined; + if (!userPermission?.roles?.[0]) return undefined; // when introducting cron mode change it here const activeRoles = - userPermission?.projectMembershipRoles?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - const activeGroupRoles = - userPermission?.userGroupRoles?.filter( + userPermission?.roles?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ) ?? []; @@ -831,7 +637,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { return { ...userPermission, - roles: [...activeRoles, ...activeGroupRoles], + roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; }) @@ -841,378 +647,52 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { } }; - const getProjectPermission: TPermissionDALFactory["getProjectPermission"] = async ( - userId: string, - projectId: string - ) => { - try { - const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId"); - const docs = await db - .replicaNode()(TableName.Users) - .where(`${TableName.Users}.id`, userId) - .leftJoin(TableName.GroupProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId])) - // @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it - .andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups); - }) - .leftJoin( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - { groupCustomRoles: TableName.ProjectRoles }, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `groupCustomRoles.id` - ) - .leftJoin(TableName.ProjectMembership, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`); - }) - .leftJoin( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => { - void queryBuilder - .on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId])) - .andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`); - }) - .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) - .join(TableName.OrgMembership, (qb) => { - void qb - .on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`); - }) - .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { - void queryBuilder - .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`); - }) - .select( - db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("username").withSchema(TableName.Users).as("username"), - // groups specific - db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"), - db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"), - db - .ref("customRoleId") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleCustomRoleId"), - db - .ref("isTemporary") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleIsTemporary"), - db - .ref("temporaryMode") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryMode"), - db - .ref("temporaryRange") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.GroupProjectMembershipRole) - .as("userGroupProjectMembershipRoleTemporaryAccessEndTime"), - // user specific - db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), - db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserMembershipRole) - .as("userProjectMembershipRoleTemporaryAccessEndTime"), - db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"), - db - .ref("permissions") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryMode"), - db - .ref("isTemporary") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryRange"), - db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.ProjectUserAdditionalPrivilege) - .as("userAdditionalPrivilegesTemporaryAccessEndTime"), - // general - db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), - db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), - db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), - db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), - db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), - db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"), - db.ref("orgId").withSchema(TableName.Project), - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("id").withSchema(TableName.Project).as("projectId"), - db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization) - ); - - const [userPermission] = sqlNestRelationships({ - data: docs, - key: "projectId", - parentMapper: ({ - orgId, - username, - orgAuthEnforced, - orgGoogleSsoAuthEnforced, - orgRole, - membershipId, - groupMembershipId, - membershipCreatedAt, - groupMembershipCreatedAt, - groupMembershipUpdatedAt, - membershipUpdatedAt, - projectType, - shouldUseNewPrivilegeSystem, - bypassOrgAuthEnabled - }) => ({ - orgId, - orgAuthEnforced, - orgGoogleSsoAuthEnforced, - orgRole: orgRole as OrgMembershipRole, - userId, - projectId, - username, - projectType, - id: membershipId || groupMembershipId, - createdAt: membershipCreatedAt || groupMembershipCreatedAt, - updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, - shouldUseNewPrivilegeSystem, - bypassOrgAuthEnabled - }), - childrenMapper: [ - { - key: "userGroupProjectMembershipRoleId", - label: "userGroupRoles" as const, - mapper: ({ - userGroupProjectMembershipRoleId, - userGroupProjectMembershipRole, - userGroupProjectMembershipRolePermission, - userGroupProjectMembershipRoleCustomRoleSlug, - userGroupProjectMembershipRoleIsTemporary, - userGroupProjectMembershipRoleTemporaryMode, - userGroupProjectMembershipRoleTemporaryAccessEndTime, - userGroupProjectMembershipRoleTemporaryAccessStartTime, - userGroupProjectMembershipRoleTemporaryRange - }) => ({ - id: userGroupProjectMembershipRoleId, - role: userGroupProjectMembershipRole, - customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug, - permissions: userGroupProjectMembershipRolePermission, - temporaryRange: userGroupProjectMembershipRoleTemporaryRange, - temporaryMode: userGroupProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userGroupProjectMembershipRoleIsTemporary - }) - }, - { - key: "userProjectMembershipRoleId", - label: "projecMembershiptRoles" as const, - mapper: ({ - userProjectMembershipRoleId, - userProjectMembershipRole, - userProjectCustomRolePermission, - userProjectMembershipRoleIsTemporary, - userProjectMembershipRoleTemporaryMode, - userProjectMembershipRoleTemporaryRange, - userProjectMembershipRoleTemporaryAccessEndTime, - userProjectMembershipRoleTemporaryAccessStartTime, - userProjectMembershipRoleCustomRoleSlug - }) => ({ - id: userProjectMembershipRoleId, - role: userProjectMembershipRole, - customRoleSlug: userProjectMembershipRoleCustomRoleSlug, - permissions: userProjectCustomRolePermission, - temporaryRange: userProjectMembershipRoleTemporaryRange, - temporaryMode: userProjectMembershipRoleTemporaryMode, - temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime, - temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime, - isTemporary: userProjectMembershipRoleIsTemporary - }) - }, - { - key: "userAdditionalPrivilegesId", - label: "additionalPrivileges" as const, - mapper: ({ - userAdditionalPrivilegesId, - userAdditionalPrivilegesPermissions, - userAdditionalPrivilegesIsTemporary, - userAdditionalPrivilegesTemporaryMode, - userAdditionalPrivilegesTemporaryRange, - userAdditionalPrivilegesTemporaryAccessEndTime, - userAdditionalPrivilegesTemporaryAccessStartTime - }) => ({ - id: userAdditionalPrivilegesId, - permissions: userAdditionalPrivilegesPermissions, - temporaryRange: userAdditionalPrivilegesTemporaryRange, - temporaryMode: userAdditionalPrivilegesTemporaryMode, - temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime, - temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime, - isTemporary: userAdditionalPrivilegesIsTemporary - }) - }, - { - key: "metadataId", - label: "metadata" as const, - mapper: ({ metadataKey, metadataValue, metadataId }) => ({ - id: metadataId, - key: metadataKey, - value: metadataValue - }) - } - ] - }); - - if (!userPermission) return undefined; - if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projecMembershiptRoles?.[0]) return undefined; - - // when introducting cron mode change it here - const activeRoles = - userPermission?.projecMembershiptRoles?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - const activeGroupRoles = - userPermission?.userGroupRoles?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - const activeAdditionalPrivileges = - userPermission?.additionalPrivileges?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ) ?? []; - - return { - ...userPermission, - roles: [...activeRoles, ...activeGroupRoles], - additionalPrivileges: activeAdditionalPrivileges - }; - } catch (error) { - throw new DatabaseError({ error, name: "GetProjectPermission" }); - } - }; - const getProjectIdentityPermissions: TPermissionDALFactory["getProjectIdentityPermissions"] = async ( - projectId: string + projectId: string, + orgId: string ) => { try { const docs = await db - .replicaNode()(TableName.IdentityProjectMembership) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join( - // Join the Project table to later select orgId - TableName.Project, - `${TableName.IdentityProjectMembership}.projectId`, - `${TableName.Project}.id` - ) + .replicaNode()(TableName.Membership) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.AdditionalPrivilege, (qb) => { + qb.on(`${TableName.Membership}.actorIdentityId`, `${TableName.AdditionalPrivilege}.actorIdentityId`).andOn( + `${TableName.Membership}.scopeOrgId`, + `${TableName.AdditionalPrivilege}.orgId` + ); + }) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .select(selectAllTableCols(TableName.MembershipRole)) .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Membership).as("membershipId"), db.ref("id").withSchema(TableName.Identity).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles), - db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), - db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryRange"), + db.ref("createdAt").withSchema(TableName.Membership).as("membershipCreatedAt"), + db.ref("updatedAt").withSchema(TableName.Membership).as("membershipUpdatedAt"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.Role), + db.ref("id").withSchema(TableName.AdditionalPrivilege).as("identityApId"), + db.ref("permissions").withSchema(TableName.AdditionalPrivilege).as("identityApPermissions"), + db.ref("temporaryMode").withSchema(TableName.AdditionalPrivilege).as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.AdditionalPrivilege).as("identityApIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.AdditionalPrivilege).as("identityApTemporaryRange"), db .ref("temporaryAccessStartTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("identityApTemporaryAccessStartTime"), db .ref("temporaryAccessEndTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .withSchema(TableName.AdditionalPrivilege) .as("identityApTemporaryAccessEndTime"), db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), @@ -1222,15 +702,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { const permissions = sqlNestRelationships({ data: docs, key: "identityId", - parentMapper: ({ - membershipId, - membershipCreatedAt, - membershipUpdatedAt, - orgId, - identityName, - projectType, - identityId - }) => ({ + parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, identityName, identityId }) => ({ id: membershipId, identityId, username: identityName, @@ -1238,7 +710,6 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { createdAt: membershipCreatedAt, updatedAt: membershipUpdatedAt, orgId, - projectType, // just a prefilled value orgAuthEnforced: false }), @@ -1247,7 +718,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { key: "id", label: "roles" as const, mapper: (data) => - IdentityProjectMembershipRoleSchema.extend({ + MembershipRolesSchema.extend({ permissions: z.unknown(), customRoleSlug: z.string().optional().nullable() }).parse(data) @@ -1309,170 +780,10 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { } }; - const getProjectIdentityPermission: TPermissionDALFactory["getProjectIdentityPermission"] = async ( - identityId, - projectId - ) => { - try { - const docs = await db - .replicaNode()(TableName.IdentityProjectMembership) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .join( - // Join the Project table to later select orgId - TableName.Project, - `${TableName.IdentityProjectMembership}.projectId`, - `${TableName.Project}.id` - ) - .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) - .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { - void queryBuilder - .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`); - }) - .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .select(selectAllTableCols(TableName.IdentityProjectMembershipRole)) - .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"), - db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project - db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("permissions").withSchema(TableName.ProjectRoles), - db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), - db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), - db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), - db - .ref("temporaryMode") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), - db - .ref("temporaryRange") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryAccessEndTime"), - db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), - db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") - ); - - const permission = sqlNestRelationships({ - data: docs, - key: "membershipId", - parentMapper: ({ - membershipId, - membershipCreatedAt, - membershipUpdatedAt, - orgId, - identityName, - projectType, - shouldUseNewPrivilegeSystem - }) => ({ - id: membershipId, - identityId, - username: identityName, - projectId, - createdAt: membershipCreatedAt, - updatedAt: membershipUpdatedAt, - orgId, - projectType, - shouldUseNewPrivilegeSystem, - // just a prefilled value - orgAuthEnforced: false - }), - childrenMapper: [ - { - key: "id", - label: "roles" as const, - mapper: (data) => - IdentityProjectMembershipRoleSchema.extend({ - permissions: z.unknown(), - customRoleSlug: z.string().optional().nullable() - }).parse(data) - }, - { - key: "identityApId", - label: "additionalPrivileges" as const, - mapper: ({ - identityApId, - identityApPermissions, - identityApIsTemporary, - identityApTemporaryMode, - identityApTemporaryRange, - identityApTemporaryAccessEndTime, - identityApTemporaryAccessStartTime - }) => ({ - id: identityApId, - permissions: identityApPermissions, - temporaryRange: identityApTemporaryRange, - temporaryMode: identityApTemporaryMode, - temporaryAccessEndTime: identityApTemporaryAccessEndTime, - temporaryAccessStartTime: identityApTemporaryAccessStartTime, - isTemporary: identityApIsTemporary - }) - }, - { - key: "metadataId", - label: "metadata" as const, - mapper: ({ metadataKey, metadataValue, metadataId }) => ({ - id: metadataId, - key: metadataKey, - value: metadataValue - }) - } - ] - }); - - if (!permission?.[0]) return undefined; - - // when introducting cron mode change it here - const activeRoles = permission?.[0]?.roles.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ); - const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter( - ({ isTemporary, temporaryAccessEndTime }) => - !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) - ); - - return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; - } catch (error) { - throw new DatabaseError({ error, name: "GetProjectIdentityPermission" }); - } - }; - return { - getOrgPermission, - getOrgIdentityPermission, - getProjectPermission, - getProjectIdentityPermission, getProjectUserPermissions, getProjectIdentityPermissions, - getProjectGroupPermissions + getProjectGroupPermissions, + getPermission }; }; diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 7d61d7499..0da5ad3b2 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; -import { OrgMembershipRole, TOrganizations } from "@app/db/schemas"; +import { TOrganizations } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; @@ -123,13 +123,13 @@ function validateOrgSSO( isOrgSsoEnforced: TOrganizations["authEnforced"], isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"], isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"], - orgRole: OrgMembershipRole + isAdmin: boolean ) { if (actorAuthMethod === undefined) { throw new UnauthorizedError({ name: "No auth method defined" }); } - if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) { + if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && isAdmin) { return; } diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index f25b84bd5..1f0e00470 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -1,8 +1,8 @@ -import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { MongoAbility } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; import { Knex } from "knex"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, TMemberships } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionSet } from "./org-permission"; @@ -49,232 +49,90 @@ export type TGetProjectPermissionArg = { actionProjectType: ActionProjectType; }; +export type TGetOrgPermissionArg = { + actor: ActorType; + actorId: string; + orgId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId?: string; +}; + export type TPermissionServiceFactory = { - getUserOrgPermission: ( - userId: string, - orgId: string, - authMethod: ActorAuthMethod, - userOrgId?: string - ) => Promise<{ - permission: MongoAbility; - membership: { - status: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - role: string; - isActive: boolean; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - permissions?: unknown; - userId?: string | null | undefined; - roleId?: string | null | undefined; - inviteEmail?: string | null | undefined; - projectFavorites?: string[] | null | undefined; - customRoleSlug?: string | null | undefined; - orgAuthEnforced?: boolean | null | undefined; - } & { - groups: { - id: string; - updatedAt: Date; - createdAt: Date; - role: string; - roleId: string | null | undefined; - customRolePermission: unknown; - name: string; - slug: string; - orgId: string; - }[]; - }; - }>; getOrgPermission: ( type: ActorType, id: string, orgId: string, authMethod: ActorAuthMethod, actorOrgId: string | undefined - ) => Promise< - | { - permission: MongoAbility; - membership: { - status: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - role: string; - isActive: boolean; - shouldUseNewPrivilegeSystem: boolean; - bypassOrgAuthEnabled: boolean; - permissions?: unknown; - userId?: string | null | undefined; - roleId?: string | null | undefined; - inviteEmail?: string | null | undefined; - projectFavorites?: string[] | null | undefined; - customRoleSlug?: string | null | undefined; - orgAuthEnforced?: boolean | null | undefined; - } & { - groups: { - id: string; - updatedAt: Date; - createdAt: Date; - role: string; - roleId: string | null | undefined; - customRolePermission: unknown; - name: string; - slug: string; - orgId: string; - }[]; - }; + ) => Promise<{ + permission: MongoAbility; + memberships: Array< + TMemberships & { + roles: { role: string; customRoleSlug?: string | null }[]; + shouldUseNewPrivilegeSystem?: boolean | null; } - | { - permission: MongoAbility; - membership: { - id: string; - role: string; - createdAt: Date; - updatedAt: Date; - orgId: string; - roleId?: string | null | undefined; - permissions?: unknown; - identityId: string; - orgAuthEnforced: boolean | null | undefined; - shouldUseNewPrivilegeSystem: boolean; - }; - } - >; - getUserProjectPermission: ({ - userId, - projectId, - authMethod, - userOrgId, - actionProjectType - }: TGetUserProjectPermissionArg) => Promise<{ - permission: MongoAbility; - membership: { - id: string; - createdAt: Date; - updatedAt: Date; - userId: string; - projectId: string; - } & { - orgAuthEnforced: boolean | null | undefined; - orgId: string; - roles: Array<{ - role: string; - }>; - shouldUseNewPrivilegeSystem: boolean; - }; + >; hasRole: (role: string) => boolean; }>; - getProjectPermission: ( - arg: TGetProjectPermissionArg - ) => Promise< - T extends ActorType.SERVICE - ? { - permission: MongoAbility; - membership: { - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (arg: string) => boolean; - } - : { - permission: MongoAbility; - membership: (T extends ActorType.USER - ? { - id: string; - createdAt: Date; - updatedAt: Date; - userId: string; - projectId: string; - } - : { - id: string; - createdAt: Date; - updatedAt: Date; - projectId: string; - identityId: string; - }) & { - orgAuthEnforced: boolean | null | undefined; - orgId: string; - roles: Array<{ - role: string; - }>; - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (role: string) => boolean; - } - >; - getProjectPermissions: (projectId: string) => Promise<{ + getProjectPermission: (arg: TGetProjectPermissionArg) => Promise<{ + permission: MongoAbility; + memberships: Array; + hasRole: (role: string) => boolean; + }>; + getProjectPermissions: ( + projectId: string, + orgId: string + ) => Promise<{ userPermissions: { permission: MongoAbility; id: string; name: string; - membershipId: string; }[]; identityPermissions: { permission: MongoAbility; id: string; name: string; - membershipId: string; }[]; groupPermissions: { permission: MongoAbility; id: string; name: string; - membershipId: string; }[]; }>; - getOrgPermissionByRole: ( - role: string, + getOrgPermissionByRoles: ( + roles: string[], orgId: string ) => Promise< - | { - permission: MongoAbility; - role: { - name: string; - orgId: string; - id: string; - createdAt: Date; - updatedAt: Date; - slug: string; - permissions?: unknown; - description?: string | null | undefined; - }; - } - | { - permission: MongoAbility; - role?: undefined; - } + { + permission: MongoAbility; + role?: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + slug: string; + permissions?: unknown; + description?: string | null | undefined; + }; + }[] >; - getProjectPermissionByRole: ( - role: string, + getProjectPermissionByRoles: ( + roles: string[], projectId: string ) => Promise< - | { - permission: MongoAbility; - role: { - name: string; - version: number; - id: string; - createdAt: Date; - updatedAt: Date; - projectId: string; - slug: string; - permissions?: unknown; - description?: string | null | undefined; - }; - } - | { - permission: MongoAbility; - role?: undefined; - } + { + permission: MongoAbility; + role?: { + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + slug: string; + permissions?: unknown; + description?: string | null | undefined; + }; + }[] >; - buildOrgPermission: (orgUserRoles: TBuildOrgPermissionDTO) => MongoAbility; - buildProjectPermissionRules: ( - projectUserRoles: TBuildProjectPermissionDTO - ) => RawRuleOf>[]; checkGroupProjectPermission: ({ groupId, projectId, diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 9e94e1415..2d879b4fd 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -1,17 +1,15 @@ import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { MongoQuery } from "@ucast/mongo2js"; import handlebars from "handlebars"; import { Knex } from "knex"; import { + AccessScope, ActionProjectType, OrgMembershipRole, ProjectMembershipRole, - ServiceTokenScopes, - TIdentityProjectMemberships, - TProjectMemberships + ServiceTokenScopes } from "@app/db/schemas"; import { cryptographicOperatorPermissions, @@ -25,12 +23,12 @@ import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/ import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { objectify } from "@app/lib/fn"; -import { logger } from "@app/lib/logger"; import { ActorType } from "@app/services/auth/auth-type"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; +import { TRoleDALFactory } from "@app/services/role/role-dal"; import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; @@ -38,56 +36,86 @@ import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns"; import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO, - TGetIdentityProjectPermissionArg, - TGetProjectPermissionArg, TGetServiceTokenProjectPermissionArg, - TGetUserProjectPermissionArg, TPermissionServiceFactory } from "./permission-service-types"; import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission"; +const buildOrgPermissionRules = (orgUserRoles: TBuildOrgPermissionDTO) => { + const rules = orgUserRoles + .map(({ role, permissions }) => { + switch (role) { + case OrgMembershipRole.Admin: + return orgAdminPermissions; + case OrgMembershipRole.Member: + return orgMemberPermissions; + case OrgMembershipRole.NoAccess: + return orgNoAccessPermissions; + case OrgMembershipRole.Custom: + return unpackRules>>( + permissions as PackRule>>[] + ); + default: + throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` }); + } + }) + .reduce((prev, curr) => prev.concat(curr), []); + + return rules; +}; + +const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { + const rules = projectUserRoles + .map(({ role, permissions }) => { + switch (role) { + case ProjectMembershipRole.Admin: + return projectAdminPermissions; + case ProjectMembershipRole.Member: + return projectMemberPermissions; + case ProjectMembershipRole.Viewer: + return projectViewerPermission; + case ProjectMembershipRole.NoAccess: + return projectNoAccessPermissions; + case ProjectMembershipRole.SshHostBootstrapper: + return sshHostBootstrapPermissions; + case ProjectMembershipRole.KmsCryptographicOperator: + return cryptographicOperatorPermissions; + case ProjectMembershipRole.Custom: { + return unpackRules>>( + permissions as PackRule>>[] + ); + } + default: + throw new NotFoundError({ + name: "ProjectRoleInvalid", + message: `Project role '${role}' not found` + }); + } + }) + .reduce((prev, curr) => prev.concat(curr), []); + + return rules; +}; + type TPermissionServiceFactoryDep = { - orgRoleDAL: Pick; - projectRoleDAL: Pick; serviceTokenDAL: Pick; projectDAL: Pick; permissionDAL: TPermissionDALFactory; keyStore: TKeyStoreFactory; + userDAL: Pick; + identityDAL: Pick; + roleDAL: Pick; }; export const permissionServiceFactory = ({ permissionDAL, - orgRoleDAL, - projectRoleDAL, serviceTokenDAL, projectDAL, - keyStore + userDAL, + identityDAL, + keyStore, + roleDAL }: TPermissionServiceFactoryDep): TPermissionServiceFactory => { - const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { - const rules = orgUserRoles - .map(({ role, permissions }) => { - switch (role) { - case OrgMembershipRole.Admin: - return orgAdminPermissions; - case OrgMembershipRole.Member: - return orgMemberPermissions; - case OrgMembershipRole.NoAccess: - return orgNoAccessPermissions; - case OrgMembershipRole.Custom: - return unpackRules>>( - permissions as PackRule>>[] - ); - default: - throw new NotFoundError({ name: "OrgRoleInvalid", message: `Organization role '${role}' not found` }); - } - }) - .reduce((prev, curr) => prev.concat(curr), []); - - return createMongoAbility(rules, { - conditionsMatcher - }); - }; - const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => { const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId); await keyStore.pgIncrementBy(projectPermissionDalVersionKey, { @@ -97,147 +125,59 @@ export const permissionServiceFactory = ({ }); }; - const calculateProjectPermissionTtl = (membership: unknown): number => { - const now = new Date(); - let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds; - - const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => { - return items - .filter((item) => item.isTemporary && item.temporaryAccessEndTime) - .map((item) => item.temporaryAccessEndTime!) - .filter((endTime) => endTime > now) - .reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null); - }; - - const roleTimes: Date[] = []; - const additionalPrivilegeTimes: Date[] = []; - - if ( - membership && - typeof membership === "object" && - "roles" in membership && - Array.isArray((membership as Record).roles) - ) { - const roles = (membership as Record).roles as Array<{ - temporaryAccessEndTime?: Date | null; - isTemporary?: boolean; - }>; - const minRoleEndTime = getMinEndTime(roles); - if (minRoleEndTime) roleTimes.push(minRoleEndTime); - } - - if ( - membership && - typeof membership === "object" && - "additionalPrivileges" in membership && - Array.isArray((membership as Record).additionalPrivileges) - ) { - const additionalPrivileges = (membership as Record).additionalPrivileges as Array<{ - temporaryAccessEndTime?: Date | null; - isTemporary?: boolean; - }>; - const minAdditionalEndTime = getMinEndTime(additionalPrivileges); - if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime); - } - - const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes]; - if (allEndTimes.length > 0) { - const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min)); - const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000); - minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry)); - } - - return minTtl; - }; - - const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { - const rules = projectUserRoles - .map(({ role, permissions }) => { - switch (role) { - case ProjectMembershipRole.Admin: - return projectAdminPermissions; - case ProjectMembershipRole.Member: - return projectMemberPermissions; - case ProjectMembershipRole.Viewer: - return projectViewerPermission; - case ProjectMembershipRole.NoAccess: - return projectNoAccessPermissions; - case ProjectMembershipRole.SshHostBootstrapper: - return sshHostBootstrapPermissions; - case ProjectMembershipRole.KmsCryptographicOperator: - return cryptographicOperatorPermissions; - case ProjectMembershipRole.Custom: { - return unpackRules>>( - permissions as PackRule>>[] - ); - } - default: - throw new NotFoundError({ - name: "ProjectRoleInvalid", - message: `Project role '${role}' not found` - }); - } - }) - .reduce((prev, curr) => prev.concat(curr), []); - - return rules; - }; - - /* - * Get user permission in an organization - */ - const getUserOrgPermission: TPermissionServiceFactory["getUserOrgPermission"] = async ( - userId, - orgId, - authMethod, - userOrgId - ) => { - // when token is scoped, ensure the passed org id is same as user org id - if (userOrgId && userOrgId !== orgId) - throw new ForbiddenRequestError({ message: "Invalid user token. Scoped to different organization." }); - const membership = await permissionDAL.getOrgPermission(userId, orgId); - if (!membership) throw new ForbiddenRequestError({ name: "You are not apart of this organization" }); - if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { - throw new BadRequestError({ name: "Custom organization permission not found" }); - } - - // If the org ID is API_KEY, the request is being made with an API Key. - // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. - - // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. - // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. - if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { - throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); - } - - validateOrgSSO( - authMethod, - membership.orgAuthEnforced, - membership.orgGoogleSsoAuthEnforced, - membership.bypassOrgAuthEnabled, - membership.role as OrgMembershipRole - ); - - const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( - membership?.groups?.map(({ role, customRolePermission }) => ({ - role, - permissions: customRolePermission - })) || [] - ); - return { permission: buildOrgPermission(finalPolicyRoles), membership }; - }; - - const getIdentityOrgPermission = async (identityId: string, orgId: string) => { - const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId); - if (!membership) throw new ForbiddenRequestError({ name: "Identity is not apart of this organization" }); - if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { - throw new NotFoundError({ name: `Custom organization permission not found for identity ${identityId}` }); - } - return { - permission: buildOrgPermission([{ role: membership.role, permissions: membership.permissions }]), - membership - }; - }; + // akhilmdhh: will bring this up later + // const calculateProjectPermissionTtl = (membership: unknown): number => { + // const now = new Date(); + // let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds; + // + // const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => { + // return items + // .filter((item) => item.isTemporary && item.temporaryAccessEndTime) + // .map((item) => item.temporaryAccessEndTime!) + // .filter((endTime) => endTime > now) + // .reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null); + // }; + // + // const roleTimes: Date[] = []; + // const additionalPrivilegeTimes: Date[] = []; + // + // if ( + // membership && + // typeof membership === "object" && + // "roles" in membership && + // Array.isArray((membership as Record).roles) + // ) { + // const roles = (membership as Record).roles as Array<{ + // temporaryAccessEndTime?: Date | null; + // isTemporary?: boolean; + // }>; + // const minRoleEndTime = getMinEndTime(roles); + // if (minRoleEndTime) roleTimes.push(minRoleEndTime); + // } + // + // if ( + // membership && + // typeof membership === "object" && + // "additionalPrivileges" in membership && + // Array.isArray((membership as Record).additionalPrivileges) + // ) { + // const additionalPrivileges = (membership as Record).additionalPrivileges as Array<{ + // temporaryAccessEndTime?: Date | null; + // isTemporary?: boolean; + // }>; + // const minAdditionalEndTime = getMinEndTime(additionalPrivileges); + // if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime); + // } + // + // const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes]; + // if (allEndTimes.length > 0) { + // const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min)); + // const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000); + // minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry)); + // } + // + // return minTtl; + // }; const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( type, @@ -246,199 +186,62 @@ export const permissionServiceFactory = ({ authMethod, actorOrgId ) => { - switch (type) { - case ActorType.USER: - return getUserOrgPermission(id, orgId, authMethod, actorOrgId); - case ActorType.IDENTITY: - return getIdentityOrgPermission(id, orgId); - default: - throw new BadRequestError({ - message: "Invalid actor provided", - name: "Get org permission" - }); - } - }; - - // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like - // admin member or user defined ones like biller etc - const getOrgPermissionByRole: TPermissionServiceFactory["getOrgPermissionByRole"] = async (role, orgId) => { - const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); - if (isCustomRole) { - const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); - if (!orgRole) - throw new NotFoundError({ - message: `Specified role '${role}' was not found in the organization with ID '${orgId}'` - }); - return { - permission: buildOrgPermission([{ role: OrgMembershipRole.Custom, permissions: orgRole.permissions }]), - role: orgRole - }; - } - return { permission: buildOrgPermission([{ role, permissions: [] }]) }; - }; - - // user permission for a project in an organization - const getUserProjectPermission = async ({ - userId, - projectId, - authMethod, - userOrgId, - actionProjectType - }: TGetUserProjectPermissionArg): Promise> => { - const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); - if (!userProjectPermission) throw new ForbiddenRequestError({ name: "User not a part of the specified project" }); - - if ( - userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) - ) { - throw new NotFoundError({ name: "The permission was not found" }); + if (type !== ActorType.USER && type !== ActorType.IDENTITY) { + throw new BadRequestError({ + message: "Invalid actor provided", + name: "Get org permission" + }); } - // If the org ID is API_KEY, the request is being made with an API Key. - // Since we can't scope API keys to an organization, we'll need to do an arbitrary check to see if the user is a member of the organization. - - // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. - // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. - if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) { + if (orgId !== actorOrgId) { throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } + const permissionData = await permissionDAL.getPermission({ + scopeData: { + scope: AccessScope.Organization, + orgId + }, + actorId: id, + actorType: type + }); + if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + + const permissionFromRoles = permissionData.flatMap((membership) => { + const activeRoles = membership?.roles + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ role, permissions }) => ({ role, permissions })); + const activeAdditionalPrivileges = membership?.additionalPrivileges + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ permissions }) => ({ role: OrgMembershipRole.Custom, permissions })); + return activeRoles.concat(activeAdditionalPrivileges); + }); + + const hasRole = (role: string) => + permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); + validateOrgSSO( authMethod, - userProjectPermission.orgAuthEnforced, - userProjectPermission.orgGoogleSsoAuthEnforced, - userProjectPermission.bypassOrgAuthEnabled, - userProjectPermission.orgRole + permissionData?.[0].orgAuthEnforced, + Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), + Boolean(permissionData?.[0].bypassOrgAuthEnabled), + hasRole(OrgMembershipRole.Admin) ); - if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { - throw new BadRequestError({ - message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` - }); - } - - // join two permissions and pass to build the final permission set - const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; - const additionalPrivileges = - userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ - role: ProjectMembershipRole.Custom, - permissions - })) || []; - - const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); - const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); - const unescapedMetadata = objectify( - userProjectPermission.metadata, - (i) => i.key, - (i) => i.value - ); - const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); - requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata }); - const interpolateRules = templatedRules( - { - identity: { - id: userProjectPermission.userId, - username: userProjectPermission.username, - metadata: metadataKeyValuePair - } - }, - { data: false } - ); - const permission = createMongoAbility( - JSON.parse(interpolateRules) as RawRuleOf>[], - { - conditionsMatcher - } - ); + const permission = createMongoAbility(buildOrgPermissionRules(permissionFromRoles), { + conditionsMatcher + }); return { permission, - membership: userProjectPermission, - hasRole: (role: string) => - userProjectPermission.roles.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 - }; - }; - - const getIdentityProjectPermission = async ({ - identityId, - projectId, - identityOrgId, - actionProjectType - }: TGetIdentityProjectPermissionArg): Promise> => { - const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); - if (!identityProjectPermission) - throw new ForbiddenRequestError({ - name: `Identity is not a member of the specified project with ID '${projectId}'` - }); - - if ( - identityProjectPermission.roles.some( - ({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions - ) - ) { - throw new NotFoundError({ name: "Custom permission not found" }); - } - - if (identityProjectPermission.orgId !== identityOrgId) { - throw new ForbiddenRequestError({ name: "Identity is not a member of the specified organization" }); - } - - if (actionProjectType !== ActionProjectType.Any && actionProjectType !== identityProjectPermission.projectType) { - throw new BadRequestError({ - message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${actionProjectType} are not allowed.` - }); - } - - const rolePermissions = - identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; - const additionalPrivileges = - identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ - role: ProjectMembershipRole.Custom, - permissions - })) || []; - - const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges)); - const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); - const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo"); - const unescapedMetadata = objectify( - identityProjectPermission.metadata, - (i) => i.key, - (i) => i.value - ); - const identityAuthInfo = - unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo - ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth") - : {}; - const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); - - requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo }); - const interpolateRules = templatedRules( - { - identity: { - id: identityProjectPermission.identityId, - username: identityProjectPermission.username, - metadata: metadataKeyValuePair, - auth: identityAuthInfo - } - }, - { data: false } - ); - const permission = createMongoAbility( - JSON.parse(interpolateRules) as RawRuleOf>[], - { - conditionsMatcher - } - ); - - return { - permission, - membership: identityProjectPermission, - hasRole: (role: string) => - identityProjectPermission.roles.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 + memberships: permissionData, + hasRole }; }; @@ -455,10 +258,6 @@ export const permissionServiceFactory = ({ if (!serviceTokenProject) throw new BadRequestError({ message: "Service token not linked to a project" }); - if (serviceTokenProject.orgId !== actorOrgId) { - throw new ForbiddenRequestError({ message: "Service token not a part of the specified organization" }); - } - if (serviceToken.projectId !== projectId) { throw new ForbiddenRequestError({ name: `Service token not a part of the specified project with ID ${projectId}` @@ -480,34 +279,156 @@ export const permissionServiceFactory = ({ const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), - membership: { - shouldUseNewPrivilegeSystem: true - } + memberships: [], + hasRole: () => false }; }; - type TProjectPermissionRT = T extends ActorType.SERVICE - ? { - permission: MongoAbility; - membership: { - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (arg: string) => boolean; - } // service token doesn't have both membership and roles - : { - permission: MongoAbility; - membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & { - orgAuthEnforced: boolean | null | undefined; - orgId: string; - roles: Array<{ role: string }>; - shouldUseNewPrivilegeSystem: boolean; - }; - hasRole: (role: string) => boolean; - }; + const getProjectPermission: TPermissionServiceFactory["getProjectPermission"] = async ({ + actor: inputActor, + actorId: inputActorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType + }) => { + let actor = inputActor; + let actorId = inputActorId; - const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId) => { + if (actor === ActorType.SERVICE) { + return getServiceTokenProjectPermission({ + serviceTokenId: actorId, + projectId, + actorOrgId, + actionProjectType + }); + } + + const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); + if ( + assumedPrivilegeDetailsCtx && + actor === ActorType.USER && + actorId === assumedPrivilegeDetailsCtx.requesterId && + projectId === assumedPrivilegeDetailsCtx.projectId + ) { + actor = assumedPrivilegeDetailsCtx.actorType; + actorId = assumedPrivilegeDetailsCtx.actorId; + } + + if (ActorType.USER !== actor && ActorType.IDENTITY !== actor) { + throw new BadRequestError({ + message: "Invalid actor provided", + name: "Get org permission" + }); + } + + const projectDetails = await projectDAL.findById(projectId); + if (!projectDetails) { + throw new NotFoundError({ message: `Project with ${projectId} not found` }); + } + + if (projectDetails.orgId !== actorOrgId) { + throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); + } + + if (actionProjectType !== ActionProjectType.Any && actionProjectType !== projectDetails.type) { + throw new BadRequestError({ + message: `The project is of type ${projectDetails.type}. Operations of type ${actionProjectType} are not allowed.` + }); + } + + const permissionData = await permissionDAL.getPermission({ + scopeData: { + scope: AccessScope.Project, + orgId: projectDetails.orgId, + projectId + }, + actorId, + actorType: actor + }); + if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + + const permissionFromRoles = permissionData.flatMap((membership) => { + const activeRoles = membership?.roles + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ role, permissions }) => ({ role, permissions })); + const activeAdditionalPrivileges = membership?.additionalPrivileges + .filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ) + .map(({ permissions }) => ({ role: ProjectMembershipRole.Custom, permissions })); + return activeRoles.concat(activeAdditionalPrivileges); + }); + + const hasRole = (role: string) => + permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); + + validateOrgSSO( + actorAuthMethod, + permissionData?.[0].orgAuthEnforced, + Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), + Boolean(permissionData?.[0].bypassOrgAuthEnabled), + hasRole(ProjectMembershipRole.Admin) + ); + + const rules = buildProjectPermissionRules(permissionFromRoles); + const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false }); + const unescapedMetadata = objectify( + permissionData?.[0]?.metadata, + (i) => i.key, + (i) => i.value + ); + const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata"); + requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata }); + + let username = ""; + if (actor === ActorType.USER) { + const userDetails = await userDAL.findById(actorId); + username = userDetails?.username; + } else { + const identityDetails = await identityDAL.findById(actorId); + username = identityDetails?.name; + } + + const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo"); + const identityAuthInfo = + unescapedIdentityAuthInfo?.identityId === actorId && unescapedIdentityAuthInfo + ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth") + : {}; + + const interpolateRules = templatedRules( + { + identity: { + id: actorId, + username, + metadata: metadataKeyValuePair, + auth: identityAuthInfo + } + }, + { data: false } + ); + + const permission = createMongoAbility( + JSON.parse(interpolateRules) as RawRuleOf>[], + { + conditionsMatcher + } + ); + + return { + permission, + memberships: permissionData, + hasRole + }; + }; + + const getProjectPermissions: TPermissionServiceFactory["getProjectPermissions"] = async (projectId, orgId) => { // fetch user permissions - const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId); + const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId, orgId); const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => { const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; @@ -547,13 +468,12 @@ export const permissionServiceFactory = ({ return { permission, id: userProjectPermission.userId, - name: userProjectPermission.username, - membershipId: userProjectPermission.id + name: userProjectPermission.username }; }); // fetch identity permissions - const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId); + const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId, orgId); const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => { const rolePermissions = identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; @@ -593,8 +513,7 @@ export const permissionServiceFactory = ({ return { permission, id: identityProjectPermission.identityId, - name: identityProjectPermission.username, - membershipId: identityProjectPermission.id + name: identityProjectPermission.username }; }); @@ -611,8 +530,7 @@ export const permissionServiceFactory = ({ return { permission, id: groupProjectPermission.groupId, - name: groupProjectPermission.username, - membershipId: groupProjectPermission.id + name: groupProjectPermission.username }; }); @@ -623,141 +541,105 @@ export const permissionServiceFactory = ({ }; }; - const getProjectPermission = async ({ - actor: inputActor, - actorId: inputActorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType - }: TGetProjectPermissionArg): Promise> => { - let actor = inputActor; - let actorId = inputActorId; - const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); - if ( - assumedPrivilegeDetailsCtx && - actor === ActorType.USER && - actorId === assumedPrivilegeDetailsCtx.requesterId && - projectId === assumedPrivilegeDetailsCtx.projectId - ) { - actor = assumedPrivilegeDetailsCtx.actorType; - actorId = assumedPrivilegeDetailsCtx.actorId; + // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like + // admin member or user defined ones like biller etc + const getOrgPermissionByRoles: TPermissionServiceFactory["getOrgPermissionByRoles"] = async (roles, orgId) => { + const formattedRoles = roles.map((role) => ({ + name: role, + isCustom: !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole) + })); + + const customRoles = formattedRoles.filter((el) => el.isCustom).map((el) => el.name); + const customRoleDetails = customRoles.length + ? await roleDAL.find({ + orgId, + $in: { + slug: customRoles + } + }) + : []; + if (customRoles.length !== customRoleDetails.length) { + const missingRoles = customRoles.filter((role) => !customRoleDetails.find((el) => el.slug === role)); + throw new NotFoundError({ + message: `Specified roles '${missingRoles.join(",")}' was not found in the organization with ID '${orgId}'` + }); } - if (actor === ActorType.SERVICE) { - return getServiceTokenProjectPermission({ - serviceTokenId: actorId, - projectId, - actorOrgId, - actionProjectType - }) as Promise>; - } - - const cachedProjectPermissionVersion = await keyStore.pgGetIntItem( - KeyStorePrefixes.ProjectPermissionDalVersion(projectId) - ); - const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0); - const cacheKey = KeyStorePrefixes.ProjectPermission( - projectId, - projectPermissionVersion, - actor, - actorId, - actionProjectType || ActionProjectType.Any - ); - - try { - const cachedData = await keyStore.getItem(cacheKey); - if (cachedData) { - const parsed = JSON.parse(cachedData) as { - rules: RawRuleOf>[]; - membership: { - roles?: Array<{ role: string; customRoleSlug?: string }>; - [key: string]: unknown; - }; - }; - const permission = createMongoAbility(parsed.rules, { - conditionsMatcher - }); - + return formattedRoles.map((el) => { + if (el.isCustom) { + const roleDetails = customRoleDetails.find((role) => role.slug === el.name); return { - permission, - membership: parsed.membership, - hasRole: (role: string) => - parsed.membership.roles?.findIndex( - ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug - ) !== -1 - } as TProjectPermissionRT; + permission: createMongoAbility( + buildOrgPermissionRules([{ role: OrgMembershipRole.Custom, permissions: roleDetails?.permissions || [] }]), + { + conditionsMatcher + } + ), + role: roleDetails! + }; } - } catch (error) { - logger.error(error, "Failed to get project permission"); - } - let result: TProjectPermissionRT; - - switch (actor) { - case ActorType.USER: - result = (await getUserProjectPermission({ - userId: actorId, - projectId, - authMethod: actorAuthMethod, - userOrgId: actorOrgId, - actionProjectType - })) as TProjectPermissionRT; - break; - case ActorType.IDENTITY: - result = (await getIdentityProjectPermission({ - identityId: actorId, - projectId, - identityOrgId: actorOrgId, - actionProjectType - })) as TProjectPermissionRT; - break; - default: - throw new BadRequestError({ - message: "Invalid actor provided", - name: "Get project permission" - }); - } - - try { - const cacheData = { - rules: result.permission.rules, - membership: result.membership + return { + permission: createMongoAbility( + buildOrgPermissionRules([{ role: el.name, permissions: [] }]), + { + conditionsMatcher + } + ) }; - - const ttl = calculateProjectPermissionTtl(result.membership); - await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData)); - } catch (error) { - logger.error(error, "Failed to cache project permission"); - } - - return result; + }); }; - const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async ( - role, + const getProjectPermissionByRoles: TPermissionServiceFactory["getProjectPermissionByRoles"] = async ( + roles, projectId ) => { - const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); - if (isCustomRole) { - const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); - if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` }); - const rules = buildProjectPermissionRules([ - { role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } - ]); - return { - permission: createMongoAbility(rules, { - conditionsMatcher - }), - role: projectRole - }; + const formattedRoles = roles.map((role) => ({ + name: role, + isCustom: !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) + })); + + const customRoles = formattedRoles.filter((el) => el.isCustom).map((el) => el.name); + const customRoleDetails = customRoles.length + ? await roleDAL.find({ + projectId, + $in: { + slug: customRoles + } + }) + : []; + if (customRoles.length !== customRoleDetails.length) { + const missingRoles = customRoles.filter((role) => !customRoleDetails.find((el) => el.slug === role)); + throw new NotFoundError({ + message: `Specified roles '${missingRoles.join(",")}' was not found in the project with ID '${projectId}'` + }); } - const rules = buildProjectPermissionRules([{ role, permissions: [] }]); - const permission = createMongoAbility(rules, { - conditionsMatcher + return formattedRoles.map((el) => { + if (el.isCustom) { + const roleDetails = customRoleDetails.find((role) => role.slug === el.name); + return { + permission: createMongoAbility( + buildProjectPermissionRules([ + { role: ProjectMembershipRole.Custom, permissions: roleDetails?.permissions || [] } + ]), + { + conditionsMatcher + } + ), + role: roleDetails! + }; + } + + return { + permission: createMongoAbility( + buildProjectPermissionRules([{ role: el.name, permissions: [] }]), + { + conditionsMatcher + } + ) + }; }); - return { permission }; }; const checkGroupProjectPermission: TPermissionServiceFactory["checkGroupProjectPermission"] = async ({ @@ -785,15 +667,11 @@ export const permissionServiceFactory = ({ }; return { - getUserOrgPermission, getOrgPermission, - getUserProjectPermission, getProjectPermission, getProjectPermissions, - getOrgPermissionByRole, - getProjectPermissionByRole, - buildOrgPermission, - buildProjectPermissionRules, + getOrgPermissionByRoles, + getProjectPermissionByRoles, checkGroupProjectPermission, invalidateProjectPermissionCache }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4c7f1faac..d2c5b30db 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,6 +1,7 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; +import { ProjectMembershipRole } from "@app/db/schemas"; import { CASL_ACTION_SCHEMA_ENUM, CASL_ACTION_SCHEMA_NATIVE_ENUM @@ -199,6 +200,9 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export const isCustomProjectRole = (slug: string) => + !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); + export enum ProjectPermissionSub { Role = "role", Member = "member", diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts deleted file mode 100644 index 6a3be2631..000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify, TOrmify } from "@app/lib/knex"; - -export type TProjectUserAdditionalPrivilegeDALFactory = TOrmify; - -export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient): TProjectUserAdditionalPrivilegeDALFactory => { - const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege); - return orm; -}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts deleted file mode 100644 index c2876572f..000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ /dev/null @@ -1,388 +0,0 @@ -import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; - -import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { ms } from "@app/lib/ms"; -import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; -import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; -import { ActorType } from "@app/services/auth/auth-type"; -import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; - -import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal"; -import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; -import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { - ProjectPermissionMemberActions, - ProjectPermissionSet, - ProjectPermissionSub -} from "../permission/project-permission"; -import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; -import { - ProjectUserAdditionalPrivilegeTemporaryMode, - TProjectUserAdditionalPrivilegeServiceFactory -} from "./project-user-additional-privilege-types"; - -type TProjectUserAdditionalPrivilegeServiceFactoryDep = { - projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; - projectMembershipDAL: Pick; - permissionService: Pick; - accessApprovalRequestDAL: Pick; -}; - -const unpackPermissions = (permissions: unknown) => - UnpackedPermissionSchema.array().parse( - unpackRules((permissions || []) as PackRule>>[]) - ); - -export const projectUserAdditionalPrivilegeServiceFactory = ({ - projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, - permissionService, - accessApprovalRequestDAL -}: TProjectUserAdditionalPrivilegeServiceFactoryDep): TProjectUserAdditionalPrivilegeServiceFactory => { - const create: TProjectUserAdditionalPrivilegeServiceFactory["create"] = async ({ - slug, - actor, - actorId, - permissions: customPermission, - actorOrgId, - actorAuthMethod, - projectMembershipId, - ...dto - }) => { - const projectMembership = await projectMembershipDAL.findById(projectMembershipId); - if (!projectMembership) - throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: projectMembership.userId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - targetUserPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged user", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ - slug, - projectId: projectMembership.projectId, - userId: projectMembership.userId - }); - if (existingSlug) - throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); - - validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const packedPermission = JSON.stringify(packRules(customPermission)); - if (!dto.isTemporary) { - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ - userId: projectMembership.userId, - projectId: projectMembership.projectId, - slug, - permissions: packedPermission - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ - projectId: projectMembership.projectId, - userId: projectMembership.userId, - slug, - permissions: packedPermission, - isTemporary: true, - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: dto.temporaryRange, - temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const updateById: TProjectUserAdditionalPrivilegeServiceFactory["updateById"] = async ({ - privilegeId, - actorOrgId, - actor, - actorId, - actorAuthMethod, - ...dto - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: projectMembership.userId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - - // we need to validate that the privilege given is not higher than the assigning users permission - // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules - targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - targetUserPermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to update more privileged user", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - - if (dto?.slug) { - const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ - slug: dto.slug, - userId: projectMembership.id, - projectId: projectMembership.projectId - }); - if (existingSlug && existingSlug.id !== userPrivilege.id) - throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` }); - } - - validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), { - allowedExpressions: (val) => val.includes("identity.") - }); - - const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; - - const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions)); - if (isTemporary) { - const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; - const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - slug: dto.slug, - permissions: packedPermission, - isTemporary: dto.isTemporary, - temporaryRange: dto.temporaryRange, - temporaryMode: dto.temporaryMode, - temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), - temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - } - - const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - slug: dto.slug, - permissions: packedPermission, - isTemporary: false, - temporaryAccessStartTime: null, - temporaryAccessEndTime: null, - temporaryRange: null, - temporaryMode: null - }); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) - }; - }; - - const deleteById: TProjectUserAdditionalPrivilegeServiceFactory["deleteById"] = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - privilegeId - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - - await accessApprovalRequestDAL.update( - { - privilegeId: userPrivilege.id - }, - { - privilegeDeletedAt: new Date(), - status: ApprovalStatus.REJECTED - } - ); - const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - - await permissionService.invalidateProjectPermissionCache(projectMembership.projectId); - - return { - ...deletedPrivilege, - permissions: unpackPermissions(deletedPrivilege.permissions) - }; - }; - - const getPrivilegeDetailsById: TProjectUserAdditionalPrivilegeServiceFactory["getPrivilegeDetailsById"] = async ({ - privilegeId, - actorOrgId, - actor, - actorId, - actorAuthMethod - }) => { - const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); - if (!userPrivilege) - throw new NotFoundError({ message: `User additional privilege with ID ${privilegeId} not found` }); - - const projectMembership = await projectMembershipDAL.findOne({ - userId: userPrivilege.userId, - projectId: userPrivilege.projectId - }); - if (!projectMembership) - throw new NotFoundError({ - message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` - }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - return { - ...userPrivilege, - permissions: unpackPermissions(userPrivilege.permissions) - }; - }; - - const listPrivileges: TProjectUserAdditionalPrivilegeServiceFactory["listPrivileges"] = async ({ - projectMembershipId, - actorOrgId, - actor, - actorId, - actorAuthMethod - }) => { - const projectMembership = await projectMembershipDAL.findById(projectMembershipId); - if (!projectMembership) - throw new NotFoundError({ message: `Project membership with ID ${projectMembershipId} not found` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: projectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); - - const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( - { - userId: projectMembership.userId, - projectId: projectMembership.projectId - }, - { sort: [[`${TableName.ProjectUserAdditionalPrivilege}.slug` as "slug", "asc"]] } - ); - return userPrivileges; - }; - - return { - create, - updateById, - deleteById, - getPrivilegeDetailsById, - listPrivileges - }; -}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts deleted file mode 100644 index a700d997d..000000000 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TProjectUserAdditionalPrivilege } from "@app/db/schemas"; -import { TProjectPermission } from "@app/lib/types"; - -import { TProjectPermissionV2Schema } from "../permission/project-permission"; - -export enum ProjectUserAdditionalPrivilegeTemporaryMode { - Relative = "relative" -} - -export type TCreateUserPrivilegeDTO = ( - | { - permissions: TProjectPermissionV2Schema[]; - projectMembershipId: string; - slug: string; - isTemporary: false; - } - | { - permissions: TProjectPermissionV2Schema[]; - projectMembershipId: string; - slug: string; - isTemporary: true; - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - } -) & - Omit; - -export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit & - Partial<{ - permissions: TProjectPermissionV2Schema[]; - slug: string; - isTemporary: boolean; - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; - -export type TDeleteUserPrivilegeDTO = Omit & { privilegeId: string }; - -export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string }; - -export type TListUserPrivilegesDTO = Omit & { projectMembershipId: string }; - -interface TAdditionalPrivilege extends TProjectUserAdditionalPrivilege { - permissions: { - action: string[]; - subject?: string | undefined; - conditions?: unknown; - inverted?: boolean | undefined; - }[]; -} - -export type TProjectUserAdditionalPrivilegeServiceFactory = { - create: (arg: TCreateUserPrivilegeDTO) => Promise; - updateById: (arg: TUpdateUserPrivilegeDTO) => Promise; - deleteById: (arg: TDeleteUserPrivilegeDTO) => Promise; - getPrivilegeDetailsById: (arg: TGetUserPrivilegeDetailsDTO) => Promise; - listPrivileges: (arg: TListUserPrivilegesDTO) => Promise; -}; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index f1ee313e8..ab84ebd39 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -4,6 +4,7 @@ import { Knex } from "knex"; import RE2 from "re2"; import { + AccessScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -19,13 +20,13 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { AuthTokenType } 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 { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; @@ -68,32 +69,30 @@ type TSamlConfigServiceFactoryDep = { "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; identityMetadataDAL: Pick; - orgMembershipDAL: Pick; - groupDAL: Pick; - userGroupMembershipDAL: Pick< - TUserGroupMembershipDALFactory, - "find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership" - >; - groupProjectDAL: Pick; - projectDAL: Pick; - projectBotDAL: Pick; - projectKeyDAL: Pick; + membershipRoleDAL: Pick; permissionService: Pick; licenseService: Pick; tokenService: Pick; smtpService: Pick; kmsService: Pick; + userGroupMembershipDAL: Pick< + TUserGroupMembershipDALFactory, + "find" | "delete" | "transaction" | "insertMany" | "filterProjectsByUserMembership" + >; + groupDAL: Pick; + projectDAL: Pick; + projectBotDAL: Pick; + projectKeyDAL: Pick; + membershipGroupDAL: Pick; }; export const samlConfigServiceFactory = ({ samlConfigDAL, orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, groupDAL, userGroupMembershipDAL, - groupProjectDAL, projectDAL, projectBotDAL, projectKeyDAL, @@ -102,7 +101,9 @@ export const samlConfigServiceFactory = ({ tokenService, smtpService, identityMetadataDAL, - kmsService + kmsService, + membershipRoleDAL, + membershipGroupDAL }: TSamlConfigServiceFactoryDep): TSamlConfigServiceFactory => { const parseSamlGroups = (groupsValue: string): string[] => { let samlGroups: string[] = []; @@ -195,10 +196,10 @@ export const samlConfigServiceFactory = ({ userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx: transaction }); } catch (error) { @@ -218,7 +219,7 @@ export const samlConfigServiceFactory = ({ group, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx: transaction }); @@ -506,26 +507,35 @@ export const samlConfigServiceFactory = ({ const foundUser = await userDAL.findById(userAlias.userId, tx); const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); + if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: userAlias.userId, + actorUserId: userAlias.userId, inviteEmail: email, - orgId, - role, - roleId, - status: foundUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, + scopeOrgId: orgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); // Only update the membership to Accepted if the user account is already completed. } else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) { await orgDAL.updateMembershipById( @@ -606,8 +616,9 @@ export const samlConfigServiceFactory = ({ const [orgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: newUser.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: userAlias.userId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -617,15 +628,22 @@ export const samlConfigServiceFactory = ({ const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole); - await orgMembershipDAL.create( + const membership = await orgDAL.createMembership( { - userId: newUser.id, - inviteEmail: email, - orgId, - role, - roleId, + actorUserId: newUser.id, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: newUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later - isActive: true + isActive: true, + inviteEmail: email.toLowerCase() + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId }, tx ); diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index 9cc6e134d..a08cd5dbf 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -2,7 +2,15 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { scimPatch } from "scim-patch"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas"; +import { + AccessScope, + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TGroups, + TMemberships, + TUsers +} from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -11,18 +19,19 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { TAdditionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; import { AuthTokenType } from "@app/services/auth/auth-type"; import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; +import { TMembershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; -import { 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 { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; -import { 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"; @@ -33,7 +42,6 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns"; import { TScimGroup, TScimServiceFactory } from "./scim-types"; @@ -55,12 +63,8 @@ type TScimServiceFactoryDep = { | "updateMembershipById" | "findOrgById" >; - orgMembershipDAL: Pick< - TOrgMembershipDALFactory, - "find" | "findOne" | "create" | "updateById" | "findById" | "update" - >; + membershipUserDAL: TMembershipUserDALFactory; projectDAL: Pick; - projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, | "create" @@ -72,7 +76,8 @@ type TScimServiceFactoryDep = { | "updateById" | "update" >; - groupProjectDAL: Pick; + membershipGroupDAL: Pick; + membershipRoleDAL: TMembershipRoleDALFactory; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, | "find" @@ -88,8 +93,8 @@ type TScimServiceFactoryDep = { licenseService: Pick; permissionService: Pick; smtpService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; }; export const scimServiceFactory = ({ @@ -98,18 +103,18 @@ export const scimServiceFactory = ({ userDAL, userAliasDAL, orgDAL, - orgMembershipDAL, projectDAL, - projectMembershipDAL, groupDAL, - groupProjectDAL, userGroupMembershipDAL, projectKeyDAL, projectBotDAL, permissionService, - projectUserAdditionalPrivilegeDAL, smtpService, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + membershipGroupDAL, + membershipUserDAL, + membershipRoleDAL, + additionalPrivilegeDAL }: TScimServiceFactoryDep): TScimServiceFactory => { const createScimToken: TScimServiceFactory["createScimToken"] = async ({ actor, @@ -244,8 +249,9 @@ export const scimServiceFactory = ({ const getScimUser: TScimServiceFactory["getScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -322,13 +328,14 @@ export const scimServiceFactory = ({ const { user: createdUser, orgMembership: createdOrgMembership } = await userDAL.transaction(async (tx) => { let user: TUsers | undefined; - let orgMembership: TOrgMemberships; + let orgMembership: TMemberships; if (userAlias) { user = await userDAL.findById(userAlias.userId, tx); - orgMembership = await orgMembershipDAL.findOne( + orgMembership = await membershipUserDAL.findOne( { - userId: user.id, - orgId + actorUserId: user.id, + scope: AccessScope.Organization, + scopeOrgId: orgId }, tx ); @@ -336,20 +343,27 @@ export const scimServiceFactory = ({ if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); - orgMembership = await orgMembershipDAL.create( + orgMembership = await membershipUserDAL.create( { - userId: userAlias.userId, + actorUserId: userAlias.userId, inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: orgMembership.id, + role, + customRoleId: roleId + }, + tx + ); } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { - orgMembership = await orgMembershipDAL.updateById( + orgMembership = await membershipUserDAL.updateById( orgMembership.id, { status: OrgMembershipStatus.Accepted @@ -401,8 +415,9 @@ export const scimServiceFactory = ({ const [foundOrgMembership] = await orgDAL.findMembership( { - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }, { tx } ); @@ -412,18 +427,25 @@ export const scimServiceFactory = ({ if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole); - orgMembership = await orgMembershipDAL.create( + orgMembership = await membershipUserDAL.create( { - userId: user.id, + actorUserId: user.id, inviteEmail: email.toLowerCase(), - orgId, - role, - roleId, + scopeOrgId: orgId, + scope: AccessScope.Organization, status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: orgMembership.id, + role, + customRoleId: roleId + }, + tx + ); // Only update the membership to Accepted if the user account is already completed. } else if (orgMembership.status === OrgMembershipStatus.Invited && user.isAccepted) { orgMembership = await orgDAL.updateMembershipById( @@ -475,8 +497,9 @@ export const scimServiceFactory = ({ const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -485,7 +508,7 @@ export const scimServiceFactory = ({ }); }); - if (!membership) + if (!membership || !membership.actorUserId) throw new ScimRequestError({ detail: "User not found", status: 404 @@ -514,7 +537,7 @@ export const scimServiceFactory = ({ org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails; await userDAL.transaction(async (tx) => { - await orgMembershipDAL.updateById( + await membershipUserDAL.updateById( membership.id, { isActive: scimUser.active @@ -523,7 +546,7 @@ export const scimServiceFactory = ({ ); const hasEmailChanged = scimUser.emails[0].value !== membership.email; await userDAL.updateById( - membership.userId, + membership.actorUserId as string, { firstName: scimUser.name.givenName, email: scimUser.emails[0].value.toLowerCase(), @@ -556,8 +579,9 @@ export const scimServiceFactory = ({ const [membership] = await orgDAL .findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }) .catch(() => { throw new ScimRequestError({ @@ -566,7 +590,7 @@ export const scimServiceFactory = ({ }); }); - if (!membership) + if (!membership || !membership.actorUserId) throw new ScimRequestError({ detail: "User not found", status: 404 @@ -587,7 +611,7 @@ export const scimServiceFactory = ({ { orgId, aliasType: org.orgAuthMethod === OrgAuthMethod.OIDC ? UserAliasType.OIDC : UserAliasType.SAML, - userId: membership.userId + userId: membership.actorUserId as string }, { externalId @@ -595,7 +619,7 @@ export const scimServiceFactory = ({ tx ); - await orgMembershipDAL.updateById( + await membershipUserDAL.updateById( membership.id, { isActive: active @@ -603,7 +627,7 @@ export const scimServiceFactory = ({ tx ); await userDAL.updateById( - membership.userId, + membership.actorUserId!, { firstName, email: email?.toLowerCase(), @@ -628,8 +652,9 @@ export const scimServiceFactory = ({ const deleteScimUser: TScimServiceFactory["deleteScimUser"] = async ({ orgMembershipId, orgId }) => { const [membership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.id` as "id"]: orgMembershipId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.id` as "id"]: orgMembershipId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!membership) @@ -645,15 +670,17 @@ export const scimServiceFactory = ({ }); } - await deleteOrgMembershipFn({ - orgMembershipId: membership.id, - orgId: membership.orgId, + await deleteOrgMembershipsFn({ + orgMembershipIds: [membership.id], + orgId: membership.scopeOrgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, - licenseService + licenseService, + membershipUserDAL, + membershipRoleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); return {}; // intentionally return empty object upon success @@ -750,8 +777,9 @@ export const scimServiceFactory = ({ if (!externalGroupMapping) return; // only get org memberships that are new (invites) - const newOrgMemberships = await orgMembershipDAL.find({ + const newOrgMemberships = await membershipUserDAL.find({ status: "invited", + scope: AccessScope.Organization, $in: { id: members.map((member) => member.value) } @@ -760,15 +788,15 @@ export const scimServiceFactory = ({ if (!newOrgMemberships.length) return; // set new membership roles to group mapping value - await orgMembershipDAL.update( + await membershipRoleDAL.update( { $in: { - id: newOrgMemberships.map((membership) => membership.id) + membershipId: newOrgMemberships.map((membership) => membership.id) } }, { role: externalGroupMapping.role, - roleId: externalGroupMapping.roleId + customRoleId: externalGroupMapping.roleId } ); }; @@ -821,8 +849,26 @@ export const scimServiceFactory = ({ tx ); + const groupMembership = await membershipGroupDAL.create( + { + scope: AccessScope.Organization, + actorGroupId: group.id, + scopeOrgId: orgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: groupMembership.id, + role: OrgMembershipRole.NoAccess + }, + tx + ); + if (members && members.length) { - const orgMemberships = await orgMembershipDAL.find({ + const orgMemberships = await membershipUserDAL.find({ + scope: AccessScope.Organization, $in: { id: members.map((member) => member.value) } @@ -830,14 +876,14 @@ export const scimServiceFactory = ({ const newMembers = await addUsersToGroupByUserIds({ group, - userIds: orgMemberships.map((membership) => membership.userId as string), + userIds: orgMemberships.map((membership) => membership.actorUserId as string), userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); @@ -850,9 +896,10 @@ export const scimServiceFactory = ({ }); const orgMemberships = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: newGroup.newMembers.map((member) => member.id) + [`${TableName.Membership}.actorUserId` as "actorUserId"]: newGroup.newMembers.map((member) => member.id) } }); @@ -895,9 +942,10 @@ export const scimServiceFactory = ({ .then((g) => g.members); const orgMemberships = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.userId` as "userId"]: users + [`${TableName.Membership}.actorUserId` as "actorUserId"]: users .filter((user) => user.isPartOfGroup) .map((user) => user.id) } @@ -933,10 +981,10 @@ export const scimServiceFactory = ({ } const updatedGroup = await groupDAL.transaction(async (tx) => { - if (group.name !== displayName) { + if (group?.name !== displayName) { await externalGroupOrgRoleMappingDAL.update( { - groupName: group.name, + groupName: group?.name, orgId }, { @@ -958,14 +1006,16 @@ export const scimServiceFactory = ({ } const orgMemberships = members.length - ? await orgMembershipDAL.find({ + ? await membershipUserDAL.find({ + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, $in: { id: members.map((member) => member.value) } }) : []; - const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId)); + const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.actorUserId as string)); const userGroupMembers = await userGroupMembershipDAL.find({ groupId: group.id }); @@ -978,20 +1028,20 @@ export const scimServiceFactory = ({ const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds); const allMembersUserIdsSet = new Set(allMembersUserIds); - const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string)); + const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.actorUserId as string)); const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId)); if (toAddUserIds.length) { await addUsersToGroupByUserIds({ group, - userIds: toAddUserIds.map((member) => member.userId as string), + userIds: toAddUserIds.map((member) => member.actorUserId as string), userDAL, userGroupMembershipDAL, orgDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); } @@ -1002,7 +1052,7 @@ export const scimServiceFactory = ({ userIds: toRemoveUserIds, userDAL, userGroupMembershipDAL, - groupProjectDAL, + membershipGroupDAL, projectKeyDAL, tx }); diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 01caef223..7597dcfd4 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -2,9 +2,10 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, SecretApprovalRequestsSchema, TableName, - TOrgMemberships, + TMemberships, TSecretApprovalRequests, TSecretApprovalRequestsSecrets, TUserGroupMembership, @@ -36,6 +37,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .where(filter) .join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .join( TableName.SecretApprovalPolicy, `${TableName.SecretApprovalRequest}.policyId`, @@ -109,24 +111,22 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `secretApprovalReviewerUser.id` ) - .leftJoin( - db(TableName.OrgMembership).as("approverOrgMembership"), - `${TableName.SecretApprovalPolicyApprover}.approverUserId`, - `approverOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("approverGroupOrgMembership"), - `secretApprovalPolicyGroupApproverUser.id`, - `approverGroupOrgMembership.userId` - ) - - .leftJoin( - db(TableName.OrgMembership).as("reviewerOrgMembership"), - `${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, - `reviewerOrgMembership.userId` - ) + .leftJoin(db(TableName.Membership).as("approverOrgMembership"), (qb) => { + qb.on(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, `approverOrgMembership.actorUserId`) + .andOn(`approverOrgMembership.scopeOrgId`, `${TableName.Project}.orgId`) + .andOn(`approverOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) + .leftJoin(db(TableName.Membership).as("approverGroupOrgMembership"), (qb) => { + qb.on(`secretApprovalPolicyGroupApproverUser.id`, `approverGroupOrgMembership.actorUserId`) + .andOn(`approverGroupOrgMembership.scopeOrgId`, `${TableName.Project}.orgId`) + .andOn(`approverGroupOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) + .leftJoin(db(TableName.Membership).as("reviewerOrgMembership"), (qb) => { + qb.on(`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`, `reviewerOrgMembership.actorUserId`) + .andOn(`reviewerOrgMembership.scopeOrgId`, `${TableName.Project}.orgId`) + .andOn(`reviewerOrgMembership.scope`, db.raw("?", [AccessScope.Organization])); + }) .select(selectAllTableCols(TableName.SecretApprovalRequest)) .select( tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index aa6d4f66a..0846532f9 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -44,10 +44,7 @@ type TSshHostGroupServiceFactoryDep = { sshHostLoginUserDAL: Pick; sshHostLoginUserMappingDAL: Pick; userDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission" - >; + permissionService: Pick; licenseService: Pick; groupDAL: Pick; }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-fns.ts b/backend/src/ee/services/ssh-host/ssh-host-fns.ts index dec15e093..7e188a5fe 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-fns.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-fns.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { ActionProjectType } from "@app/db/schemas"; import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission"; import { TCreateSshLoginMappingsDTO } from "./ssh-host-types"; @@ -59,11 +60,12 @@ export const createSshLoginMappings = async ({ for await (const user of users) { // check that each user has access to the SSH project - await permissionService.getUserProjectPermission({ - userId: user.id, + await permissionService.getProjectPermission({ + actor: ActorType.USER, + actorId: user.id, projectId, - authMethod: actorAuthMethod, - userOrgId: actorOrgId, + actorAuthMethod, + actorOrgId, actionProjectType: ActionProjectType.SSH }); } diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index d1082b4df..37d843c47 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -64,10 +64,7 @@ type TSshHostServiceFactoryDep = { >; sshHostLoginUserDAL: TSshHostLoginUserDALFactory; sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission" - >; + permissionService: Pick; kmsService: Pick; }; diff --git a/backend/src/ee/services/ssh-host/ssh-host-types.ts b/backend/src/ee/services/ssh-host/ssh-host-types.ts index a8269ac37..698f067f2 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-types.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-types.ts @@ -66,7 +66,7 @@ type BaseCreateSshLoginMappingsDTO = { sshHostLoginUserDAL: Pick; sshHostLoginUserMappingDAL: Pick; userDAL: Pick; - permissionService: Pick; + permissionService: Pick; groupDAL: Pick; projectId: string; actorAuthMethod: ActorAuthMethod; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 406daa63d..824f5384c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -50,9 +50,6 @@ import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal"; import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service"; -import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; -import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; -import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service"; @@ -79,8 +76,6 @@ import { permissionServiceFactory } from "@app/ee/services/permission/permission import { pitServiceFactory } from "@app/ee/services/pit/pit-service"; import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal"; import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; -import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; -import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; import { instanceRelayConfigDalFactory } from "@app/ee/services/relay/instance-relay-config-dal"; @@ -147,6 +142,8 @@ import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; import { registerSecretScanningV2Webhooks } from "@app/server/plugins/secret-scanner-v2"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; +import { additionalPrivilegeDALFactory } from "@app/services/additional-privilege/additional-privilege-dal"; +import { additionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; @@ -174,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"; @@ -187,7 +185,6 @@ 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 { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-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"; @@ -214,7 +211,6 @@ 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 { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-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"; @@ -231,6 +227,14 @@ import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal"; import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal"; import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { kmsServiceFactory } from "@app/services/kms/kms-service"; +import { membershipDALFactory } from "@app/services/membership/membership-dal"; +import { membershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { membershipGroupDALFactory } from "@app/services/membership-group/membership-group-dal"; +import { membershipGroupServiceFactory } from "@app/services/membership-group/membership-group-service"; +import { membershipIdentityDALFactory } from "@app/services/membership-identity/membership-identity-dal"; +import { membershipIdentityServiceFactory } from "@app/services/membership-identity/membership-identity-service"; +import { membershipUserDALFactory } from "@app/services/membership-user/membership-user-dal"; +import { membershipUserServiceFactory } from "@app/services/membership-user/membership-user-service"; import { microsoftTeamsIntegrationDALFactory } from "@app/services/microsoft-teams/microsoft-teams-integration-dal"; import { microsoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { projectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; @@ -242,8 +246,6 @@ import { offlineUsageReportServiceFactory } from "@app/services/offline-usage-re import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal"; import { orgBotDALFactory } from "@app/services/org/org-bot-dal"; import { orgDALFactory } from "@app/services/org/org-dal"; -import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; -import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -274,15 +276,14 @@ import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal" import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; -import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; -import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; -import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { reminderDALFactory } from "@app/services/reminder/reminder-dal"; import { dailyReminderQueueServiceFactory } from "@app/services/reminder/reminder-queue"; import { reminderServiceFactory } from "@app/services/reminder/reminder-service"; import { reminderRecipientDALFactory } from "@app/services/reminder-recipients/reminder-recipient-dal"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { roleDALFactory } from "@app/services/role/role-dal"; +import { roleServiceFactory } from "@app/services/role/role-service"; import { secretDALFactory } from "@app/services/secret/secret-dal"; import { secretQueueFactory } from "@app/services/secret/secret-queue"; import { secretServiceFactory } from "@app/services/secret/secret-service"; @@ -382,16 +383,12 @@ export const registerRoutes = async ( const orgMembershipDAL = orgMembershipDALFactory(db); const orgBotDAL = orgBotDALFactory(db); const incidentContactDAL = incidentContactDALFactory(db); - const orgRoleDAL = orgRoleDALFactory(db); const rateLimitDAL = rateLimitDALFactory(db); const apiKeyDAL = apiKeyDALFactory(db); const projectDAL = projectDALFactory(db); const projectSshConfigDAL = projectSshConfigDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db); - const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db); - const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db); - const projectRoleDAL = projectRoleDALFactory(db); const projectEnvDAL = projectEnvDALFactory(db); const projectKeyDAL = projectKeyDALFactory(db); const projectBotDAL = projectBotDALFactory(db); @@ -423,8 +420,6 @@ export const registerRoutes = async ( const identityAccessTokenDAL = identityAccessTokenDALFactory(db); const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db); - const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); - const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db); const identityTokenAuthDAL = identityTokenAuthDALFactory(db); @@ -482,7 +477,6 @@ export const registerRoutes = async ( const gitAppOrgDAL = gitAppDALFactory(db); const groupDAL = groupDALFactory(db); const groupProjectDAL = groupProjectDALFactory(db); - const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db); const userGroupMembershipDAL = userGroupMembershipDALFactory(db); const secretScanningDAL = secretScanningDALFactory(db); const secretSharingDAL = secretSharingDALFactory(db); @@ -531,17 +525,27 @@ export const registerRoutes = async ( const secretScanningV2DAL = secretScanningV2DALFactory(db); const keyValueStoreDAL = keyValueStoreDALFactory(db); + const membershipDAL = membershipDALFactory(db); + const membershipUserDAL = membershipUserDALFactory(db); + const membershipIdentityDAL = membershipIdentityDALFactory(db); + const membershipGroupDAL = membershipGroupDALFactory(db); + const additionalPrivilegeDAL = additionalPrivilegeDALFactory(db); + const membershipRoleDAL = membershipRoleDALFactory(db); + const roleDAL = roleDALFactory(db); + const eventBusService = eventBusFactory(server.redis); const sseService = sseServiceFactory(eventBusService, server.redis); const permissionService = permissionServiceFactory({ permissionDAL, - orgRoleDAL, - projectRoleDAL, serviceTokenDAL, projectDAL, - keyStore + keyStore, + roleDAL, + userDAL, + identityDAL }); + const assumePrivilegeService = assumePrivilegeServiceFactory({ projectDAL, permissionService @@ -556,6 +560,57 @@ export const registerRoutes = async ( projectDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); + + const membershipUserService = membershipUserServiceFactory({ + licenseService, + membershipRoleDAL, + membershipUserDAL, + orgDAL, + permissionService, + roleDAL, + userDAL, + projectDAL, + projectKeyDAL, + smtpService, + tokenService, + userAliasDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL + }); + + const membershipIdentityService = membershipIdentityServiceFactory({ + membershipIdentityDAL, + membershipRoleDAL, + orgDAL, + permissionService, + roleDAL, + additionalPrivilegeDAL + }); + + const membershipGroupService = membershipGroupServiceFactory({ + membershipGroupDAL, + membershipRoleDAL, + roleDAL, + permissionService, + orgDAL + }); + + const roleService = roleServiceFactory({ + permissionService, + roleDAL, + projectDAL, + identityDAL, + userDAL, + externalGroupOrgRoleMappingDAL + }); + const additionalPrivilegeService = additionalPrivilegeServiceFactory({ + additionalPrivilegeDAL, + membershipDAL, + orgDAL, + permissionService + }); + const hsmService = hsmServiceFactory({ hsmModule, envConfig @@ -621,31 +676,29 @@ export const registerRoutes = async ( userDAL, secretApprovalRequestDAL }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const samlService = samlConfigServiceFactory({ identityMetadataDAL, permissionService, orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, samlConfigDAL, groupDAL, userGroupMembershipDAL, - groupProjectDAL, projectDAL, projectBotDAL, projectKeyDAL, licenseService, tokenService, smtpService, - kmsService + kmsService, + membershipRoleDAL, + membershipGroupDAL }); const groupService = groupServiceFactory({ userDAL, groupDAL, - groupProjectDAL, orgDAL, userGroupMembershipDAL, projectDAL, @@ -653,17 +706,13 @@ export const registerRoutes = async ( projectKeyDAL, permissionService, licenseService, - oidcConfigDAL + oidcConfigDAL, + membershipGroupDAL, + membershipRoleDAL }); const groupProjectService = groupProjectServiceFactory({ groupDAL, - groupProjectDAL, - groupProjectMembershipRoleDAL, - userGroupMembershipDAL, projectDAL, - projectKeyDAL, - projectBotDAL, - projectRoleDAL, permissionService }); @@ -708,18 +757,18 @@ export const registerRoutes = async ( userDAL, userAliasDAL, orgDAL, - orgMembershipDAL, projectDAL, - projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, - groupDAL, - groupProjectDAL, userGroupMembershipDAL, projectKeyDAL, projectBotDAL, permissionService, smtpService, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + groupDAL, + membershipGroupDAL, + membershipRoleDAL, + membershipUserDAL, + additionalPrivilegeDAL }); const githubOrgSyncConfigService = githubOrgSyncServiceFactory({ @@ -729,16 +778,16 @@ export const registerRoutes = async ( permissionService, groupDAL, userGroupMembershipDAL, - orgMembershipDAL + orgMembershipDAL, + membershipRoleDAL, + membershipGroupDAL }); const ldapService = ldapConfigServiceFactory({ ldapConfigDAL, ldapGroupMapDAL, orgDAL, - orgMembershipDAL, groupDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, @@ -749,7 +798,9 @@ export const registerRoutes = async ( licenseService, tokenService, smtpService, - kmsService + kmsService, + membershipGroupDAL, + membershipRoleDAL }); const telemetryService = telemetryServiceFactory({ @@ -771,13 +822,12 @@ export const registerRoutes = async ( const userService = userServiceFactory({ userDAL, orgDAL, - orgMembershipDAL, tokenService, permissionService, groupProjectDAL, smtpService, - projectMembershipDAL, - userAliasDAL + userAliasDAL, + membershipUserDAL }); const upgradePathService = upgradePathServiceFactory({ keyStore }); @@ -794,17 +844,18 @@ export const registerRoutes = async ( tokenService, orgDAL, totpService, - orgMembershipDAL, auditLogService, - notificationService + notificationService, + membershipRoleDAL, + membershipUserDAL }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, authDAL, userDAL, - orgMembershipDAL, - totpConfigDAL + totpConfigDAL, + membershipUserDAL }); const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL, projectDAL }); @@ -826,14 +877,10 @@ export const registerRoutes = async ( folderDAL, licenseService, samlConfigDAL, - orgRoleDAL, permissionService, orgDAL, incidentContactDAL, tokenService, - projectUserAdditionalPrivilegeDAL, - projectUserMembershipRoleDAL, - projectRoleDAL, projectDAL, projectMembershipDAL, orgMembershipDAL, @@ -846,7 +893,12 @@ export const registerRoutes = async ( ldapConfigDAL, loginService, projectBotService, - reminderService + reminderService, + membershipRoleDAL, + membershipUserDAL, + roleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); const signupService = authSignupServiceFactory({ tokenService, @@ -857,18 +909,10 @@ export const registerRoutes = async ( projectKeyDAL, projectDAL, projectBotDAL, - groupProjectDAL, - projectMembershipDAL, - projectUserMembershipRoleDAL, orgDAL, orgService, - licenseService - }); - const orgRoleService = orgRoleServiceFactory({ - permissionService, - orgRoleDAL, - orgDAL, - externalGroupOrgRoleMappingDAL + licenseService, + membershipGroupDAL }); const microsoftTeamsService = microsoftTeamsServiceFactory({ @@ -885,8 +929,6 @@ export const registerRoutes = async ( userAliasDAL, identityTokenAuthDAL, identityAccessTokenDAL, - orgMembershipDAL, - identityOrgMembershipDAL, authService: loginService, serverCfgDAL: superAdminDAL, kmsRootConfigDAL, @@ -898,7 +940,10 @@ export const registerRoutes = async ( microsoftTeamsService, invalidateCacheQueue, smtpService, - tokenService + tokenService, + membershipIdentityDAL, + membershipRoleDAL, + membershipUserDAL }); const offlineUsageReportService = offlineUsageReportServiceFactory({ @@ -910,9 +955,10 @@ export const registerRoutes = async ( smtpService, projectDAL, permissionService, - projectUserMembershipRoleDAL, - projectMembershipDAL, - notificationService + notificationService, + membershipRoleDAL, + membershipUserDAL, + projectMembershipDAL }); const rateLimitService = rateLimitServiceFactory({ @@ -938,32 +984,25 @@ export const registerRoutes = async ( const projectMembershipService = projectMembershipServiceFactory({ projectMembershipDAL, - projectUserMembershipRoleDAL, projectDAL, permissionService, - projectBotDAL, - orgDAL, userDAL, - projectUserAdditionalPrivilegeDAL, userGroupMembershipDAL, smtpService, projectKeyDAL, - projectRoleDAL, groupProjectDAL, secretReminderRecipientsDAL, licenseService, - notificationService - }); - const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({ - permissionService, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, - accessApprovalRequestDAL + notificationService, + membershipUserDAL, + additionalPrivilegeDAL, + membershipRoleDAL }); + const projectKeyService = projectKeyServiceFactory({ permissionService, projectKeyDAL, - projectMembershipDAL + membershipUserDAL }); const projectQueueService = projectQueueFactory({ @@ -979,10 +1018,10 @@ export const registerRoutes = async ( secretVersionDAL, projectKeyDAL, projectBotDAL, - projectMembershipDAL, secretApprovalRequestDAL, secretApprovalSecretDAL: secretApprovalRequestSecretDAL, - projectUserMembershipRoleDAL + membershipRoleDAL, + membershipUserDAL }); const certificateAuthorityDAL = certificateAuthorityDALFactory(db); @@ -1194,14 +1233,15 @@ export const registerRoutes = async ( snapshotSecretV2BridgeDAL, secretApprovalRequestDAL, projectKeyDAL, - projectUserMembershipRoleDAL, orgService, resourceMetadataDAL, folderCommitService, secretSyncQueue, reminderService, eventBusService, - licenseService + licenseService, + membershipRoleDAL, + membershipUserDAL }); const projectService = projectServiceFactory({ @@ -1212,13 +1252,10 @@ export const registerRoutes = async ( secretV2BridgeDAL, projectQueue: projectQueueService, projectBotService, - identityProjectDAL, - identityOrgMembershipDAL, userDAL, projectEnvDAL, orgDAL, projectMembershipDAL, - projectRoleDAL, folderDAL, licenseService, pkiSubscriberDAL, @@ -1232,8 +1269,6 @@ export const registerRoutes = async ( sshCertificateTemplateDAL, sshHostDAL, sshHostGroupDAL, - projectUserMembershipRoleDAL, - identityProjectMembershipRoleDAL, keyStore, kmsService, certificateTemplateDAL, @@ -1242,10 +1277,14 @@ export const registerRoutes = async ( projectMicrosoftTeamsConfigDAL, microsoftTeamsIntegrationDAL, projectTemplateService, - groupProjectDAL, smtpService, reminderService, - notificationService + notificationService, + membershipGroupDAL, + membershipIdentityDAL, + membershipRoleDAL, + membershipUserDAL, + roleDAL }); const projectEnvService = projectEnvServiceFactory({ @@ -1259,16 +1298,6 @@ export const registerRoutes = async ( secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL }); - const projectRoleService = projectRoleServiceFactory({ - permissionService, - projectRoleDAL, - projectUserMembershipRoleDAL, - identityProjectMembershipRoleDAL, - projectDAL, - identityDAL, - userDAL - }); - const snapshotService = secretSnapshotServiceFactory({ permissionService, licenseService, @@ -1423,21 +1452,18 @@ export const registerRoutes = async ( groupDAL, permissionService, projectEnvDAL, - projectMembershipDAL, projectDAL, userDAL, accessApprovalRequestDAL, - additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, accessApprovalRequestReviewerDAL, - orgMembershipDAL + additionalPrivilegeDAL, + membershipUserDAL }); const accessApprovalRequestService = accessApprovalRequestServiceFactory({ projectDAL, permissionService, accessApprovalRequestReviewerDAL, - additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, - projectMembershipDAL, accessApprovalPolicyDAL, accessApprovalRequestDAL, projectEnvDAL, @@ -1449,7 +1475,8 @@ export const registerRoutes = async ( groupDAL, microsoftTeamsService, projectMicrosoftTeamsConfigDAL, - notificationService + notificationService, + additionalPrivilegeDAL }); const secretReplicationService = secretReplicationServiceFactory({ @@ -1538,7 +1565,15 @@ export const registerRoutes = async ( identityProjectDAL, licenseService, identityMetadataDAL, - keyStore + keyStore, + orgDAL, + membershipIdentityDAL, + membershipRoleDAL + }); + const identityProjectService = identityProjectServiceFactory({ + identityProjectDAL, + membershipIdentityDAL, + permissionService }); const identityAuthTemplateService = identityAuthTemplateServiceFactory({ @@ -1557,105 +1592,91 @@ export const registerRoutes = async ( identityDAL }); - const identityProjectService = identityProjectServiceFactory({ - permissionService, - projectDAL, - identityProjectDAL, - identityOrgMembershipDAL, - identityProjectMembershipRoleDAL, - projectRoleDAL - }); - const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({ - projectDAL, - identityProjectAdditionalPrivilegeDAL, - permissionService, - identityProjectDAL - }); - - const identityProjectAdditionalPrivilegeV2Service = identityProjectAdditionalPrivilegeV2ServiceFactory({ - projectDAL, - identityProjectAdditionalPrivilegeDAL, - permissionService, - identityProjectDAL - }); - const identityTokenAuthService = identityTokenAuthServiceFactory({ identityTokenAuthDAL, - identityOrgMembershipDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + orgDAL, + membershipIdentityDAL }); const identityUaService = identityUaServiceFactory({ - identityOrgMembershipDAL, permissionService, identityAccessTokenDAL, identityUaClientSecretDAL, identityUaDAL, licenseService, - keyStore + keyStore, + orgDAL, + membershipIdentityDAL }); const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ identityKubernetesAuthDAL, - identityOrgMembershipDAL, identityAccessTokenDAL, permissionService, licenseService, gatewayService, + orgDAL, gatewayV2Service, gatewayV2DAL, gatewayDAL, - kmsService + kmsService, + membershipIdentityDAL }); const identityGcpAuthService = identityGcpAuthServiceFactory({ identityGcpAuthDAL, - identityOrgMembershipDAL, + orgDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + membershipIdentityDAL }); const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ identityAccessTokenDAL, + orgDAL, identityAliCloudAuthDAL, - identityOrgMembershipDAL, licenseService, - permissionService + permissionService, + membershipIdentityDAL }); const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ identityAccessTokenDAL, identityTlsCertAuthDAL, - identityOrgMembershipDAL, licenseService, permissionService, - kmsService + kmsService, + membershipIdentityDAL }); const identityAwsAuthService = identityAwsAuthServiceFactory({ identityAccessTokenDAL, + orgDAL, identityAwsAuthDAL, - identityOrgMembershipDAL, licenseService, - permissionService + permissionService, + membershipIdentityDAL }); const identityAzureAuthService = identityAzureAuthServiceFactory({ identityAzureAuthDAL, - identityOrgMembershipDAL, + orgDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + membershipIdentityDAL }); const identityOciAuthService = identityOciAuthServiceFactory({ identityAccessTokenDAL, + orgDAL, identityOciAuthDAL, - identityOrgMembershipDAL, licenseService, - permissionService + permissionService, + membershipIdentityDAL }); const pitService = pitServiceFactory({ @@ -1674,32 +1695,42 @@ export const registerRoutes = async ( const identityOidcAuthService = identityOidcAuthServiceFactory({ identityOidcAuthDAL, - identityOrgMembershipDAL, + orgDAL, identityAccessTokenDAL, permissionService, licenseService, - kmsService + kmsService, + membershipIdentityDAL }); const identityJwtAuthService = identityJwtAuthServiceFactory({ identityJwtAuthDAL, + orgDAL, permissionService, identityAccessTokenDAL, - identityOrgMembershipDAL, licenseService, - kmsService + kmsService, + membershipIdentityDAL }); const identityLdapAuthService = identityLdapAuthServiceFactory({ identityLdapAuthDAL, + orgDAL, permissionService, kmsService, identityAccessTokenDAL, - identityOrgMembershipDAL, licenseService, identityDAL, identityAuthTemplateDAL, - keyStore + keyStore, + membershipIdentityDAL + }); + + const convertorService = convertorServiceFactory({ + additionalPrivilegeDAL, + membershipDAL, + projectDAL, + groupDAL }); const dynamicSecretProviders = buildDynamicSecretProviders({ @@ -1774,7 +1805,6 @@ export const registerRoutes = async ( const oidcService = oidcConfigServiceFactory({ orgDAL, - orgMembershipDAL, userDAL, userAliasDAL, licenseService, @@ -1787,9 +1817,10 @@ export const registerRoutes = async ( projectKeyDAL, projectDAL, userGroupMembershipDAL, - groupProjectDAL, groupDAL, - auditLogService + auditLogService, + membershipGroupDAL, + membershipRoleDAL }); const userEngagementService = userEngagementServiceFactory({ @@ -1845,8 +1876,8 @@ export const registerRoutes = async ( const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({ permissionService, licenseService, - orgRoleDAL, - externalGroupOrgRoleMappingDAL + externalGroupOrgRoleMappingDAL, + roleDAL }); const appConnectionService = appConnectionServiceFactory({ @@ -2192,7 +2223,6 @@ export const registerRoutes = async ( groupProject: groupProjectService, permission: permissionService, org: orgService, - orgRole: orgRoleService, oidc: oidcService, apiKey: apiKeyService, authToken: tokenService, @@ -2202,7 +2232,6 @@ export const registerRoutes = async ( projectMembership: projectMembershipService, projectKey: projectKeyService, projectEnv: projectEnvService, - projectRole: projectRoleService, secret: secretService, secretReplication: secretReplicationService, secretTag: secretTagService, @@ -2217,7 +2246,6 @@ export const registerRoutes = async ( identity: identityService, identityAuthTemplate: identityAuthTemplateService, identityAccessToken: identityAccessTokenService, - identityProject: identityProjectService, identityTokenAuth: identityTokenAuthService, identityUa: identityUaService, identityKubernetesAuth: identityKubernetesAuthService, @@ -2264,9 +2292,6 @@ export const registerRoutes = async ( scim: scimService, secretBlindIndex: secretBlindIndexService, telemetry: telemetryService, - projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, - identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, - identityProjectAdditionalPrivilegeV2: identityProjectAdditionalPrivilegeV2Service, secretSharing: secretSharingService, userEngagement: userEngagementService, externalKms: externalKmsService, @@ -2300,7 +2325,15 @@ export const registerRoutes = async ( pamResource: pamResourceService, pamAccount: pamAccountService, pamSession: pamSessionService, - upgradePath: upgradePathService + upgradePath: upgradePathService, + + membershipUser: membershipUserService, + membershipIdentity: membershipIdentityService, + membershipGroup: membershipGroupService, + role: roleService, + additionalPrivilege: additionalPrivilegeService, + identityProject: identityProjectService, + convertor: convertorService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 344224008..47fbb0e07 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -209,11 +209,11 @@ export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivile ) }); -export const SanitizedRoleSchema = ProjectRolesSchema.extend({ +export const SanitizedRoleSchema = ProjectRolesSchema.omit({ version: true }).extend({ permissions: UnpackedPermissionSchema.array() }); -export const SanitizedRoleSchemaV1 = ProjectRolesSchema.extend({ +export const SanitizedRoleSchemaV1 = ProjectRolesSchema.omit({ version: true }).extend({ permissions: UnpackedPermissionSchema.array().transform((caslPermission) => // first map and remove other actions of folder permission caslPermission diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 03dd8ed94..ddb3f2326 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -5,6 +5,7 @@ import { IdentitiesSchema, OrganizationsSchema, OrgMembershipsSchema, + OrgMembershipStatus, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; @@ -279,7 +280,10 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { ); return { - organizationMembership + organizationMembership: { + ...organizationMembership, + status: organizationMembership?.status || OrgMembershipStatus.Accepted + } }; } }); @@ -892,7 +896,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const organizationMembership = await server.services.superAdmin.resendOrgInvite(req.params, req.permission); - return { organizationMembership }; + + return { + organizationMembership: { + ...organizationMembership, + status: organizationMembership?.status || OrgMembershipStatus.Accepted + } + }; } }); @@ -922,7 +932,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { req.params.organizationId, req.permission ); - return { organizationMembership }; + return { + organizationMembership: { + ...organizationMembership, + status: organizationMembership?.status || OrgMembershipStatus.Accepted + } + }; } }); diff --git a/backend/src/server/routes/v1/deprecated-project-membership-router.ts b/backend/src/server/routes/v1/deprecated-project-membership-router.ts index ab225929f..25e07a466 100644 --- a/backend/src/server/routes/v1/deprecated-project-membership-router.ts +++ b/backend/src/server/routes/v1/deprecated-project-membership-router.ts @@ -1,9 +1,13 @@ import { z } from "zod"; import { + AccessScope, + OrgMembershipRole, OrgMembershipsSchema, + OrgMembershipStatus, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -13,7 +17,6 @@ import { ms } from "@app/lib/ms"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ @@ -66,14 +69,23 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const memberships = await server.services.projectMembership.getProjectMemberships({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + const { data: memberships } = await server.services.membershipUser.listMemberships({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + data: {} }); - return { memberships }; + + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.workspaceId + })) + }; } }); @@ -124,15 +136,30 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.getProjectMembershipById({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - id: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + const membership = await server.services.membershipUser.getMembershipByUserId({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + selector: { + userId + } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.workspaceId + } + }; } }); @@ -241,14 +268,22 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ ...req.auditLogInfo, event: { type: EventType.ADD_BATCH_PROJECT_MEMBER, - metadata: data.map(({ userId }) => ({ - userId: userId || "", + metadata: data.map(({ actorUserId }) => ({ + userId: actorUserId || "", email: "" })) } }); - return { data, success: true }; + return { + data: data.map((el) => ({ + ...el, + orgId: req.permission.orgId, + role: OrgMembershipRole.Member, + status: el.status || OrgMembershipStatus.Accepted + })), + success: true + }; } }); @@ -282,7 +317,7 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -300,30 +335,28 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectMembership.updateProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - membershipId: req.params.membershipId, - roles: req.body.roles + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.updateMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + selector: { + userId + }, + data: { + roles: req.body.roles + } }); - // await server.services.auditLog.createAuditLog({ - // ...req.auditLogInfo, - // projectId: req.params.workspaceId, - // event: { - // type: EventType.UPDATE_USER_WORKSPACE_ROLE, - // metadata: { - // userId: membership.userId, - // newRole: req.body.role, - // oldRole: membership.role, - // email: "" - // } - // } - // }); - return { roles }; + return { roles: membership.roles.map((el) => ({ ...el, projectMembershipId: req.params.membershipId })) }; } }); @@ -352,13 +385,22 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.deleteProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, - membershipId: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.workspaceId + }, + selector: { + userId + } }); await server.services.auditLog.createAuditLog({ @@ -367,12 +409,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ event: { type: EventType.REMOVE_PROJECT_MEMBER, metadata: { - userId: membership.userId, + userId: membership.actorUserId as string, email: "" } } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.workspaceId + } + }; } }); }; diff --git a/backend/src/server/routes/v1/group-project-router.ts b/backend/src/server/routes/v1/group-project-router.ts index d07e3bd8b..93caf5035 100644 --- a/backend/src/server/routes/v1/group-project-router.ts +++ b/backend/src/server/routes/v1/group-project-router.ts @@ -1,19 +1,21 @@ import { z } from "zod"; import { + AccessScope, GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; +import { isUuidV4 } from "@app/lib/validator"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -54,7 +56,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -73,17 +75,32 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.addGroupToProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - roles: req.body.roles || [{ role: req.body.role }], - projectId: req.params.projectId, - groupIdOrName: req.params.groupIdOrName + let groupId = req.params.groupIdOrName; + if (!isUuidV4(req.params.groupIdOrName)) { + const groupDetails = await server.services.convertor.getGroupIdFromName(groupId, req.permission.orgId); + groupId = groupDetails.groupId; + } + + const { membership: groupMembership } = await server.services.membershipGroup.createMembership({ + permission: req.permission, + data: { + groupId, + roles: req.body.roles || [{ role: req.body.role, isTemporary: false }] + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -115,7 +132,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -131,17 +148,22 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const roles = await server.services.groupProject.updateGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - groupId: req.params.groupId, - roles: req.body.roles + const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + data: { + roles: req.body.roles + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { roles }; + return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) }; } }); @@ -172,16 +194,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.removeGroupFromProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - groupId: req.params.groupId, - projectId: req.params.projectId + const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -233,15 +264,17 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listGroupsInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({ + permission: req.permission, + data: {}, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMemberships }; + return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) }; } }); @@ -292,15 +325,25 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params + const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts index a9b2d9b03..4c444d704 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -78,7 +78,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index effd66a68..fe7cf7d5b 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -45,7 +45,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_AWS_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index 9053bc2e3..9ef733a25 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -40,7 +40,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_AZURE_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index b83faf9d9..91e8038a7 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -40,7 +40,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_GCP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index 373a8b927..ecffaebe1 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_JWT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 879310790..6f0500c29 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 512f253e0..ac384d216 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -168,7 +168,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_LDAP_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts index e529f300b..df19492a6 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -57,7 +57,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_OCI_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 74cd94eb5..147105aba 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_OIDC_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-project-router.ts b/backend/src/server/routes/v1/identity-project-router.ts index 9de7ed1aa..fd39c7efe 100644 --- a/backend/src/server/routes/v1/identity-project-router.ts +++ b/backend/src/server/routes/v1/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 registerIdentityProjectRouter = async (server: FastifyZodProvider) 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 registerIdentityProjectRouter = async (server: FastifyZodProvider) 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 registerIdentityProjectRouter = async (server: FastifyZodProvider) 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 registerIdentityProjectRouter = async (server: FastifyZodProvider) } }, 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 registerIdentityProjectRouter = async (server: FastifyZodProvider) } }, 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/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts index 0bb9e08ea..0503ed16f 100644 --- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index e22c41889..9a33d0651 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { @@ -393,7 +393,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH, metadata: { @@ -447,7 +447,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 6d911a88e..153e1e641 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -59,7 +59,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg?.orgId, + orgId: identityMembershipOrg.scopeOrgId, event: { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, metadata: { 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/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index d4b1ee188..8260b3a8f 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -86,7 +86,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { }); } - return { membership }; + return { membership: { ...membership, userId: req.permission.id, projectId: req.params.projectId } }; } }); }; diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index b8eb3ad6b..872b7b157 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -6,6 +6,7 @@ import { GroupsSchema, IncidentContactsSchema, OrgMembershipsSchema, + OrgMembershipStatus, OrgRolesSchema, UsersSchema } from "@app/db/schemas"; @@ -263,7 +264,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.authMethod, req.permission.orgId ); - return { users }; + return { users: users.map((el) => ({ ...el, status: el.status || OrgMembershipStatus.Accepted })) }; } }); diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index f3cdfe701..57fdad031 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -1,10 +1,11 @@ import { z } from "zod"; import { - OrgMembershipRole, + AccessScope, ProjectMembershipRole, ProjectMembershipsSchema, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -14,7 +15,6 @@ import { ms } from "@app/lib/ms"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => { server.route({ @@ -67,14 +67,23 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const memberships = await server.services.projectMembership.getProjectMemberships({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { data: memberships } = await server.services.membershipUser.listMemberships({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + data: {} }); - return { memberships }; + + return { + memberships: memberships.map((el) => ({ + ...el, + userId: el.actorUserId as string, + projectId: req.params.projectId + })) + }; } }); @@ -125,15 +134,30 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.getProjectMembershipById({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - id: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + const membership = await server.services.membershipUser.getMembershipByUserId({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + userId + } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.projectId + } + }; } }); @@ -242,20 +266,17 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider 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: { + roles: (req.body.roleSlugs || [ProjectMembershipRole.Member]).map((role) => ({ isTemporary: false, role })), + usernames: usernamesAndEmails + } }); await server.services.auditLog.createAuditLog({ @@ -263,15 +284,21 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider ...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 + })) + }; } }); @@ -305,7 +332,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -323,17 +350,28 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const roles = await server.services.projectMembership.updateProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - membershipId: req.params.membershipId, - roles: req.body.roles + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.updateMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + userId + }, + data: { + roles: req.body.roles + } }); - return { roles }; + return { roles: membership.roles.map((el) => ({ ...el, projectMembershipId: req.params.membershipId })) }; } }); @@ -396,13 +434,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 + })) + }; } }); @@ -431,13 +475,22 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const membership = await server.services.projectMembership.deleteProjectMembership({ - actorId: req.permission.id, - actor: req.permission.type, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - membershipId: req.params.membershipId + const { userId } = await server.services.convertor.userMembershipIdToUserId( + req.params.membershipId, + AccessScope.Project, + req.permission.orgId + ); + + const { membership } = await server.services.membershipUser.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + }, + selector: { + userId + } }); await server.services.auditLog.createAuditLog({ @@ -446,12 +499,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider event: { type: EventType.REMOVE_PROJECT_MEMBER, metadata: { - userId: membership.userId, + userId: membership.actorUserId as string, email: "" } } }); - return { membership }; + + return { + membership: { + ...membership, + userId, + projectId: req.params.projectId + } + }; } }); @@ -479,7 +539,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-group-project-router.ts b/backend/src/server/routes/v2/deprecated-group-project-router.ts index f0e4ee705..5be7df838 100644 --- a/backend/src/server/routes/v2/deprecated-group-project-router.ts +++ b/backend/src/server/routes/v2/deprecated-group-project-router.ts @@ -1,19 +1,21 @@ import { z } from "zod"; import { + AccessScope, GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, ProjectUserMembershipRolesSchema, + TemporaryPermissionMode, UsersSchema } from "@app/db/schemas"; import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; +import { isUuidV4 } from "@app/lib/validator"; 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 { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ @@ -54,7 +56,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -73,17 +75,32 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.addGroupToProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - roles: req.body.roles || [{ role: req.body.role }], - projectId: req.params.projectId, - groupIdOrName: req.params.groupIdOrName + let groupId = req.params.groupIdOrName; + if (!isUuidV4(req.params.groupIdOrName)) { + const groupDetails = await server.services.convertor.getGroupIdFromName(groupId, req.permission.orgId); + groupId = groupDetails.groupId; + } + + const { membership: groupMembership } = await server.services.membershipGroup.createMembership({ + permission: req.permission, + data: { + groupId, + roles: req.body.roles || [{ role: req.body.role, isTemporary: false }] + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -115,7 +132,7 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro z.object({ role: z.string(), isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), + temporaryMode: z.nativeEnum(TemporaryPermissionMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() }) @@ -131,17 +148,22 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const roles = await server.services.groupProject.updateGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId, - groupId: req.params.groupId, - roles: req.body.roles + const { membership: groupMembership } = await server.services.membershipGroup.updateMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + data: { + roles: req.body.roles + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { roles }; + return { roles: groupMembership.roles.map((el) => ({ ...el, projectMembershipId: groupMembership.id })) }; } }); @@ -172,16 +194,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.removeGroupFromProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - groupId: req.params.groupId, - projectId: req.params.projectId + const { membership: groupMembership } = await server.services.membershipGroup.deleteMembership({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); @@ -233,15 +264,17 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listGroupsInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - projectId: req.params.projectId + const { memberships: groupMemberships } = await server.services.membershipGroup.listMemberships({ + permission: req.permission, + data: {}, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMemberships }; + return { groupMemberships: groupMemberships.map((el) => ({ ...el, groupId: el.actorGroupId as string })) }; } }); @@ -292,15 +325,25 @@ export const registerDeprecatedGroupProjectRouter = async (server: FastifyZodPro } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.getGroupInProject({ - actor: req.permission.type, - actorId: req.permission.id, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - ...req.params + const { membership: groupMembership } = await server.services.membershipGroup.getMembershipByGroupId({ + permission: req.permission, + selector: { + groupId: req.params.groupId + }, + scopeData: { + scope: AccessScope.Project, + orgId: req.permission.orgId, + projectId: req.params.projectId + } }); - return { groupMembership }; + return { + groupMembership: { + ...groupMembership, + projectId: req.params.projectId, + groupId: groupMembership.actorGroupId as string + } + }; } }); 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..93320f3e0 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { OrgMembershipsSchema, + OrgMembershipStatus, ProjectMembershipsSchema, ProjectsSchema, UserEncryptionKeysSchema, @@ -63,7 +64,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.authMethod, req.permission.orgId ); - return { users }; + return { users: users.map((el) => ({ ...el, status: el.status || OrgMembershipStatus.Accepted })) }; } }); @@ -138,6 +139,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ membership: OrgMembershipsSchema.extend({ + customRoleSlug: z.string().nullish(), metadata: z .object({ key: z.string().trim().min(1), @@ -168,7 +170,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { orgId: req.params.organizationId, membershipId: req.params.membershipId }); - return { membership }; + return { membership: { ...membership, status: membership.status || OrgMembershipStatus.Accepted } }; } }); @@ -220,7 +222,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, ...req.body }); - return { membership }; + return { + membership: { + ...membership, + role: req.body.role || "", + orgId: req.params.organizationId, + status: membership.status || OrgMembershipStatus.Accepted + } + }; } }); @@ -260,7 +269,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { membershipId: req.params.membershipId, actorOrgId: req.permission.orgId }); - return { membership }; + return { + membership: { + ...membership, + status: membership.status || OrgMembershipStatus.Accepted, + role: "", + orgId: req.params.organizationId + } + }; } }); @@ -302,7 +318,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { membershipIds: req.body.membershipIds, actorOrgId: req.permission.orgId }); - return { memberships }; + return { + memberships: memberships.map((el) => ({ + ...el, + status: el?.status || OrgMembershipStatus.Accepted, + role: "", + orgId: req.params.organizationId + })) + }; } }); diff --git a/backend/src/services/additional-privilege/additional-privilege-dal.ts b/backend/src/services/additional-privilege/additional-privilege-dal.ts new file mode 100644 index 000000000..b11bd90d6 --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAdditionalPrivilegeDALFactory = ReturnType; + +export const additionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.AdditionalPrivilege); + + return orm; +}; diff --git a/backend/src/services/additional-privilege/additional-privilege-service.ts b/backend/src/services/additional-privilege/additional-privilege-service.ts new file mode 100644 index 000000000..2af9e6419 --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-service.ts @@ -0,0 +1,254 @@ +// eslint-disable-next-line simple-import-sort/imports +import { RawRule } from "@casl/ability"; +import { packRules } from "@casl/ability/extra"; + +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; + +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TAdditionalPrivilegeDALFactory } from "./additional-privilege-dal"; +import { + TAdditionalPrivilegesScopeFactory, + TCreateAdditionalPrivilegesDTO, + TDeleteAdditionalPrivilegesDTO, + TGetAdditionalPrivilegesByIdDTO, + TGetAdditionalPrivilegesByNameDTO, + TListAdditionalPrivilegesDTO, + TUpdateAdditionalPrivilegesDTO +} from "./additional-privilege-types"; +import { newNamespaceAdditionalPrivilegesFactory } from "./namespace/namespace-additional-privilege-factory"; +import { newOrgAdditionalPrivilegesFactory } from "./org/org-additional-privilege-factory"; +import { newProjectAdditionalPrivilegesFactory } from "./project/project-additional-privilege-factory"; +import { ActorType } from "../auth/auth-type"; + +type TAdditionalPrivilegeServiceFactoryDep = { + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; + permissionService: Pick; + orgDAL: Pick; + membershipDAL: Pick; +}; + +export type TAdditionalPrivilegeServiceFactory = ReturnType; + +export const additionalPrivilegeServiceFactory = ({ + additionalPrivilegeDAL, + permissionService, + orgDAL, + membershipDAL +}: TAdditionalPrivilegeServiceFactoryDep) => { + const scopeFactory: Record = { + [AccessScope.Organization]: newOrgAdditionalPrivilegesFactory({}), + [AccessScope.Project]: newProjectAdditionalPrivilegesFactory({ + membershipDAL, + orgDAL, + permissionService + }), + [AccessScope.Namespace]: newNamespaceAdditionalPrivilegesFactory({}) + }; + + const createAdditionalPrivilege = async (dto: TCreateAdditionalPrivilegesDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onCreateAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = data.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const existingSlug = await additionalPrivilegeDAL.findOne({ + name: data.name, + [dbActorField]: data.actorId, + [scope.key]: scope.value + }); + if (existingSlug) throw new BadRequestError({ message: `Additional privilege with name ${data.name} exists` }); + + validateHandlebarTemplate("Additional Privilege Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + if (!data.isTemporary) { + const additionalPrivilege = await additionalPrivilegeDAL.create({ + name: data.name, + [dbActorField]: data.actorId, + [scope.key]: scope.value, + isTemporary: data.isTemporary, + permissions: JSON.stringify(packRules(data.permissions as RawRule[])) + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + } + + if (!data.temporaryAccessStartTime || !data.temporaryRange) { + throw new BadRequestError({ message: "Temporary mode expects start time and range" }); + } + + const relativeTempAllocatedTimeInMs = ms(data.temporaryRange); + const additionalPrivilege = await additionalPrivilegeDAL.create({ + [dbActorField]: data.actorId, + [scope.key]: scope.value, + name: data.name, + isTemporary: data.isTemporary, + permissions: JSON.stringify(packRules(data.permissions as RawRule[])), + temporaryAccessEndTime: new Date( + new Date(data.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs + ), + temporaryAccessStartTime: new Date(data.temporaryAccessStartTime), + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: data.temporaryRange + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const updateAdditionalPrivilege = async (dto: TUpdateAdditionalPrivilegesDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onUpdateAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const existingPrivilege = await additionalPrivilegeDAL.findOne({ + [dbActorField]: dto.selector.actorId, + id: dto.selector.id, + [scope.key]: scope.value + }); + if (!existingPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${dto.selector.id} doesn't exist` }); + + validateHandlebarTemplate("Additional Privilege Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const updatedData = { ...existingPrivilege, ...data }; + + if (!updatedData.isTemporary) { + const additionalPrivilege = await additionalPrivilegeDAL.updateById(existingPrivilege.id, { + name: updatedData.name, + isTemporary: data.isTemporary, + permissions: data.permissions ? JSON.stringify(packRules(data.permissions as RawRule[])) : undefined + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + } + + if (!updatedData.temporaryAccessStartTime || !updatedData.temporaryRange) { + throw new BadRequestError({ message: "Temporary mode expects start time and range" }); + } + + const relativeTempAllocatedTimeInMs = ms(updatedData.temporaryRange); + const additionalPrivilege = await additionalPrivilegeDAL.updateById(existingPrivilege.id, { + name: updatedData.name, + isTemporary: updatedData.isTemporary, + permissions: JSON.stringify(packRules(updatedData.permissions as RawRule[])), + temporaryAccessEndTime: new Date( + new Date(updatedData.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs + ), + temporaryAccessStartTime: new Date(updatedData.temporaryAccessStartTime), + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: updatedData.temporaryRange + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const deleteAdditionalPrivilege = async (dto: TDeleteAdditionalPrivilegesDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onDeleteAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const existingPrivilege = await additionalPrivilegeDAL.findOne({ + id: selector.id, + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + if (!existingPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); + + const additionalPrivilege = await additionalPrivilegeDAL.deleteById(existingPrivilege.id); + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const getAdditionalPrivilegeById = async (dto: TGetAdditionalPrivilegesByIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onGetAdditionalPrivilegesByIdGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const additionalPrivilege = await additionalPrivilegeDAL.findOne({ + id: selector.id, + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + if (!additionalPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const getAdditionalPrivilegeByName = async (dto: TGetAdditionalPrivilegesByNameDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onGetAdditionalPrivilegesByIdGuard(dto); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + const scope = factory.getScopeField(dto.scopeData); + + const additionalPrivilege = await additionalPrivilegeDAL.findOne({ + name: selector.name, + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + if (!additionalPrivilege) + throw new NotFoundError({ message: `Additional privilege with name ${selector.name} doesn't exist` }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const listAdditionalPrivileges = async (dto: TListAdditionalPrivilegesDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onListAdditionalPrivilegesGuard(dto); + const scope = factory.getScopeField(dto.scopeData); + const dbActorField = dto.selector.actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"; + + const additionalPrivileges = await additionalPrivilegeDAL.find({ + [dbActorField]: dto.selector.actorId, + [scope.key]: scope.value + }); + + return { + additionalPrivileges: additionalPrivileges.map((el) => ({ + ...el, + permissions: unpackPermissions(el.permissions) + })) + }; + }; + + return { + createAdditionalPrivilege, + updateAdditionalPrivilege, + deleteAdditionalPrivilege, + getAdditionalPrivilegeById, + getAdditionalPrivilegeByName, + listAdditionalPrivileges + }; +}; diff --git a/backend/src/services/additional-privilege/additional-privilege-types.ts b/backend/src/services/additional-privilege/additional-privilege-types.ts new file mode 100644 index 000000000..eeb5a6a0b --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-types.ts @@ -0,0 +1,87 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +import { ActorType } from "../auth/auth-type"; + +export interface TAdditionalPrivilegesScopeFactory { + onCreateAdditionalPrivilegesGuard: (arg: TCreateAdditionalPrivilegesDTO) => Promise; + onUpdateAdditionalPrivilegesGuard: (arg: TUpdateAdditionalPrivilegesDTO) => Promise; + onDeleteAdditionalPrivilegesGuard: (arg: TDeleteAdditionalPrivilegesDTO) => Promise; + onListAdditionalPrivilegesGuard: (arg: TListAdditionalPrivilegesDTO) => Promise; + onGetAdditionalPrivilegesByIdGuard: ( + arg: TGetAdditionalPrivilegesByIdDTO | TGetAdditionalPrivilegesByNameDTO + ) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; +} + +export type TCreateAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + name: string; + permissions: unknown; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }; +}; + +export type TUpdateAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; + data: Partial<{ + name: string; + permissions: unknown; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }>; +}; + +export type TListAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; + +export type TDeleteAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; + +export type TGetAdditionalPrivilegesByIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; + +export type TGetAdditionalPrivilegesByNameDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + name: string; + actorId: string; + actorType: ActorType.USER | ActorType.IDENTITY; + }; +}; diff --git a/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts b/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts new file mode 100644 index 000000000..e2ccde286 --- /dev/null +++ b/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts @@ -0,0 +1,52 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TNamespaceAdditionalPrivilegesScopeFactoryDep = Record; + +export const newNamespaceAdditionalPrivilegesFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceAdditionalPrivilegesScopeFactoryDep +): TAdditionalPrivilegesScopeFactory => { + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges create not implemented" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges update not implemented" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges delete not implemented" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges list not implemented" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges get by id not implemented" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts b/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts new file mode 100644 index 000000000..cc1fbdd6c --- /dev/null +++ b/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts @@ -0,0 +1,52 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TOrgAdditionalPrivilegesScopeFactoryDep = Record; + +export const newOrgAdditionalPrivilegesFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TOrgAdditionalPrivilegesScopeFactoryDep +): TAdditionalPrivilegesScopeFactory => { + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges create not implemented" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges update not implemented" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges delete not implemented" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges list not implemented" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges get by id not implemented" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts b/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts new file mode 100644 index 000000000..4b5179a6f --- /dev/null +++ b/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts @@ -0,0 +1,219 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionIdentityActions, + ProjectPermissionMemberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, PermissionBoundaryError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TMembershipDALFactory } from "@app/services/membership/membership-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TProjectAdditionalPrivilegesScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipDAL: Pick; +}; + +export const newProjectAdditionalPrivilegesFactory = ({ + permissionService, + orgDAL, + membershipDAL +}: TProjectAdditionalPrivilegesScopeFactoryDep): TAdditionalPrivilegesScopeFactory => { + const $getPermission = (permission: OrgServiceActor, projectId: string) => { + return permissionService.getProjectPermission({ + actor: permission.type, + actorId: permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: permission.authMethod, + projectId, + actorOrgId: permission.orgId + }); + }; + + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + + const { actorType } = dto.data; + const { permission } = await $getPermission(dto.permission, scope.value); + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Edit, ProjectPermissionSub.Identity] as const); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const { permission: targetUserPermission, memberships } = await $getPermission( + { ...dto.permission, type: actorType, id: dto.data.actorId }, + scope.value + ); + + const permissionAction = + actorType === ActorType.USER + ? ProjectPermissionMemberActions.GrantPrivileges + : ProjectPermissionIdentityActions.GrantPrivileges; + const permissionSubject = + actorType === ActorType.USER ? ProjectPermissionSub.Member : ProjectPermissionSub.Identity; + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged actor", + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const membership = memberships.find( + (el) => el[actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"] === dto.data.actorId + ); + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const { permission } = await $getPermission(dto.permission, scope.value); + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Edit, ProjectPermissionSub.Identity] as const); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const { permission: targetUserPermission, memberships } = await $getPermission( + { ...dto.permission, type: actorType, id: dto.selector.actorId }, + scope.value + ); + + const permissionAction = + actorType === ActorType.USER + ? ProjectPermissionMemberActions.GrantPrivileges + : ProjectPermissionIdentityActions.GrantPrivileges; + const permissionSubject = + actorType === ActorType.USER ? ProjectPermissionSub.Member : ProjectPermissionSub.Identity; + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged actor", + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const membership = memberships.find( + (el) => el[actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"] === dto.selector.actorId + ); + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const { permission } = await $getPermission(dto.permission, scope.value); + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Edit, ProjectPermissionSub.Identity] as const); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity] as const); + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { actorType } = dto.selector; + + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity] as const); + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 613aa0766..82df0dcb1 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,12 +1,12 @@ import { Knex } from "knex"; -import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; +import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; -import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -14,7 +14,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; }; export type TAuthTokenServiceFactory = ReturnType; @@ -80,7 +80,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -208,9 +208,10 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (token.organizationId) { - const orgMembership = await orgMembershipDAL.findOne({ - userId: user.id, - orgId: token.organizationId + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: token.organizationId, + scope: AccessScope.Organization }); if (!orgMembership) { diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index d69c836e9..31a9cc5a8 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -1,6 +1,13 @@ import { Knex } from "knex"; -import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSchema } from "@app/db/schemas"; +import { + AccessScope, + OrgMembershipRole, + OrgMembershipStatus, + TableName, + TUsers, + UserDeviceSchema +} from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; @@ -14,11 +21,12 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TNotificationServiceFactory } from "../notification/notification-service"; import { NotificationType } from "../notification/notification-types"; import { TOrgDALFactory } from "../org/org-dal"; import { getDefaultOrgMembershipRole } from "../org/org-role-fns"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; import { TTotpServiceFactory } from "../totp/totp-service"; @@ -48,7 +56,8 @@ type TAuthLoginServiceFactoryDep = { smtpService: TSmtpService; totpService: Pick; auditLogService: Pick; - orgMembershipDAL: TOrgMembershipDALFactory; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; notificationService: Pick; }; @@ -58,10 +67,11 @@ export const authLoginServiceFactory = ({ tokenService, smtpService, orgDAL, - orgMembershipDAL, totpService, auditLogService, - notificationService + notificationService, + membershipUserDAL, + membershipRoleDAL }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -163,8 +173,8 @@ export const authLoginServiceFactory = ({ if (organizationId) { const org = await orgDAL.findById(organizationId); if (org) { - await orgMembershipDAL.update( - { userId: user.id, orgId: org.id }, + await membershipUserDAL.update( + { actorUserId: user.id, scopeOrgId: org.id, scope: AccessScope.Organization }, { lastLoginAuthMethod: authMethod, lastLoginTime: new Date() } ); if (org.userTokenExpiration) { @@ -858,21 +868,34 @@ export const authLoginServiceFactory = ({ } orgId = defaultOrg.id; const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "id"]: orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!orgMembership) { const { role, roleId } = await getDefaultOrgMembershipRole(defaultOrg.defaultMembershipRole); - await orgMembershipDAL.create({ - userId: user.id, - inviteEmail: email, - orgId, - role, - roleId, - status: OrgMembershipStatus.Accepted, - isActive: true + await membershipUserDAL.transaction(async (tx) => { + const membership = await membershipUserDAL.create( + { + actorUserId: user?.id, + inviteEmail: email, + scopeOrgId: orgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role, + customRoleId: roleId + }, + tx + ); }); } } @@ -895,10 +918,11 @@ export const authLoginServiceFactory = ({ if (org) { // checks for the membership and only sets the orgId / orgName if the user is a member of the specified org const orgMembership = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, - [`${TableName.OrgMembership}.orgId` as "orgId"]: org.id, - [`${TableName.OrgMembership}.isActive` as "isActive"]: true, - [`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: org.id, + [`${TableName.Membership}.isActive` as "isActive"]: true, + [`${TableName.Membership}.status` as "status"]: OrgMembershipStatus.Accepted, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (orgMembership) { diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 21a51ef3f..75f77d0fc 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -1,3 +1,4 @@ +import { AccessScope } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; @@ -6,7 +7,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; @@ -23,7 +24,7 @@ import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; userDAL: TUserDALFactory; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; totpConfigDAL: Pick; @@ -33,7 +34,7 @@ export type TAuthPasswordFactory = ReturnType; export const authPaswordServiceFactory = ({ authDAL, userDAL, - orgMembershipDAL, + membershipUserDAL, tokenService, smtpService, totpConfigDAL @@ -54,7 +55,10 @@ export const authPaswordServiceFactory = ({ const hasEmailAuth = user.authMethods?.includes(AuthMethod.EMAIL); if (!hasEmailAuth) { - const orgMemberships = await orgMembershipDAL.find({ userId: user.id }); + const orgMemberships = await membershipUserDAL.find({ + actorUserId: user.id, + scope: AccessScope.Organization + }); const lastLoginMethod = orgMemberships .filter((membership) => membership.lastLoginAuthMethod) diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 7bc3a5ef6..a2e426a2e 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,4 +1,4 @@ -import { OrgMembershipStatus, TableName } from "@app/db/schemas"; +import { AccessScope, OrgMembershipStatus, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -8,17 +8,15 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { getMinExpiresIn } from "@app/lib/fn"; import { isDisposableEmail } from "@app/lib/validator"; -import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TMembershipGroupDALFactory } from "../membership-group/membership-group-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { getServerCfg } from "../super-admin/super-admin-service"; import { TUserDALFactory } from "../user/user-dal"; @@ -42,14 +40,12 @@ type TAuthSignupDep = { projectKeyDAL: Pick; projectDAL: Pick; projectBotDAL: Pick; - groupProjectDAL: Pick; orgService: Pick; orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; licenseService: Pick; - projectMembershipDAL: Pick; - projectUserMembershipRoleDAL: Pick; + membershipGroupDAL: TMembershipGroupDALFactory; }; export type TAuthSignupFactory = ReturnType; @@ -60,11 +56,11 @@ export const authSignupServiceFactory = ({ projectKeyDAL, projectDAL, projectBotDAL, - groupProjectDAL, tokenService, smtpService, orgService, orgDAL, + membershipGroupDAL, licenseService }: TAuthSignupDep) => { // first step of signup. create user and send email @@ -200,9 +196,10 @@ export const authSignupServiceFactory = ({ organizationId ) { const [pendingOrgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, status: OrgMembershipStatus.Invited, - [`${TableName.OrgMembership}.orgId` as "orgId"]: organizationId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: organizationId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (pendingOrgMembership) { @@ -241,18 +238,18 @@ export const authSignupServiceFactory = ({ } const updatedMembersips = await orgDAL.updateMembership( - { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, - { userId: user.id, status: OrgMembershipStatus.Accepted } + { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization }, + { actorUserId: user.id, status: OrgMembershipStatus.Accepted } ); - const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; + const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))]; await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, + membershipGroupDAL, projectDAL, projectBotDAL }); @@ -351,21 +348,21 @@ export const authSignupServiceFactory = ({ ); const updatedMembersips = await orgDAL.updateMembership( - { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, - { userId: us.id, status: OrgMembershipStatus.Accepted }, + { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited, scope: AccessScope.Organization }, + { actorUserId: us.id, status: OrgMembershipStatus.Accepted }, tx ); - const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; + const uniqueOrgId = [...new Set(updatedMembersips.map(({ scopeOrgId }) => scopeOrgId))]; await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId, tx))); await convertPendingGroupAdditionsToGroupMemberships({ userIds: [user.id], userDAL, userGroupMembershipDAL, - groupProjectDAL, projectKeyDAL, projectDAL, projectBotDAL, + membershipGroupDAL, tx }); diff --git a/backend/src/services/convertor/convertor-service.ts b/backend/src/services/convertor/convertor-service.ts new file mode 100644 index 000000000..71c25e9c0 --- /dev/null +++ b/backend/src/services/convertor/convertor-service.ts @@ -0,0 +1,130 @@ +import { AccessScope } from "@app/db/schemas"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { NotFoundError } from "@app/lib/errors"; + +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TProjectDALFactory } from "../project/project-dal"; + +type TConvertorServiceFactoryDep = { + projectDAL: Pick; + membershipDAL: Pick; + groupDAL: Pick; + additionalPrivilegeDAL: Pick; +}; + +export type TConvertorServiceFactory = ReturnType; + +export const convertorServiceFactory = ({ + projectDAL, + membershipDAL, + additionalPrivilegeDAL, + groupDAL +}: TConvertorServiceFactoryDep) => { + const projectSlugToId = async (dto: { slug: string; orgId: string }) => { + const project = await projectDAL.findOne({ + orgId: dto.orgId, + slug: dto.slug + }); + if (!project) throw new NotFoundError({ message: `Project with slug ${dto.slug} not found` }); + return project; + }; + + const userMembershipIdToUserId = async (membershipId: string, scope: AccessScope, orgId: string) => { + const membership = await membershipDAL.findOne({ + scope, + id: membershipId, + scopeOrgId: orgId + }); + if (!membership || !membership.actorUserId) { + throw new NotFoundError({ message: `Membership with id ${membershipId} not found` }); + } + return { userId: membership.actorUserId, membership }; + }; + + const groupMembershipIdToGroupId = async (membershipId: string, scope: AccessScope, orgId: string) => { + const membership = await membershipDAL.findOne({ + scope, + id: membershipId, + scopeOrgId: orgId + }); + if (!membership || !membership.actorGroupId) { + throw new NotFoundError({ message: `Membership with id ${membershipId} not found` }); + } + + return { groupId: membership.actorGroupId, membership }; + }; + + const identityMembershipIdToIdentityId = async (membershipId: string, scope: AccessScope, orgId: string) => { + const membership = await membershipDAL.findOne({ + scope, + id: membershipId, + scopeOrgId: orgId + }); + if (!membership || !membership.actorIdentityId) { + throw new NotFoundError({ message: `Membership with id ${membershipId} not found` }); + } + + return { identityId: membership.actorIdentityId, membership }; + }; + + const identityIdToMembershipId = async (identityId: string, scope: AccessScope, scopeId: string) => { + let fieldName = "scopeOrgId"; + if (scope === AccessScope.Project) { + fieldName = "scopeProjectId"; + } else if (scope === AccessScope.Namespace) { + fieldName = "scopeNamespaceId"; + } + + const membership = await membershipDAL.findOne({ + scope, + actorIdentityId: identityId, + [fieldName]: scopeId + }); + + if (!membership) { + throw new NotFoundError({ message: `Identity with id ${identityId} not found` }); + } + + return { membershipId: membership.id, membership }; + }; + + const additionalPrivilegeIdToDoc = async (privilegeId: string) => { + const doc = await additionalPrivilegeDAL.findOne({ + id: privilegeId + }); + if (!doc) { + throw new NotFoundError({ message: `Privilege with id ${privilegeId} not found` }); + } + + return { privilege: doc }; + }; + const additionalPrivilegeNameToDoc = async (privilegeName: string, projectId: string) => { + const privilege = await additionalPrivilegeDAL.findOne({ + name: privilegeName, + projectId + }); + if (!privilege) { + throw new NotFoundError({ message: `Privilege with name ${privilegeName} not found` }); + } + + return { privilegeId: privilege.id, privilege }; + }; + + const getGroupIdFromName = async (name: string, orgId: string) => { + const group = await groupDAL.findOne({ orgId, name }); + if (!group) throw new NotFoundError({ message: `Failed to find group with name ${name}` }); + return { groupId: group.id, group }; + }; + + return { + projectSlugToId, + userMembershipIdToUserId, + groupMembershipIdToGroupId, + identityMembershipIdToIdentityId, + additionalPrivilegeIdToDoc, + additionalPrivilegeNameToDoc, + identityIdToMembershipId, + getGroupIdFromName + }; +}; diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts index 552eed307..21b0b3c1f 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-fns.ts @@ -1,19 +1,19 @@ -import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { OrgMembershipRole, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; +import { TRoleDALFactory } from "../role/role-dal"; import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types"; export const constructGroupOrgMembershipRoleMappings = async ({ mappingsDTO, orgId, - orgRoleDAL, + roleDAL, licenseService }: { mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[]; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; licenseService: TLicenseServiceFactory; orgId: string; }) => { @@ -30,9 +30,9 @@ export const constructGroupOrgMembershipRoleMappings = async ({ .filter((mapping) => isCustomOrgRole(mapping.roleSlug)) .map((mapping) => mapping.roleSlug); - let customRolesMap: Map = new Map(); + let customRolesMap: Map = new Map(); if (customRoleSlugs.length > 0) { - const customRoles = await orgRoleDAL.find({ + const customRoles = await roleDAL.find({ orgId, $in: { slug: customRoleSlugs diff --git a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts index de293609b..a072544ca 100644 --- a/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts +++ b/backend/src/services/external-group-org-role-mapping/external-group-org-role-mapping-service.ts @@ -6,15 +6,15 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { OrgServiceActor } from "@app/lib/types"; import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns"; import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; +import { TRoleDALFactory } from "../role/role-dal"; import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal"; type TExternalGroupOrgRoleMappingServiceFactoryDep = { externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory; permissionService: TPermissionServiceFactory; licenseService: TLicenseServiceFactory; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; }; export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType; @@ -23,7 +23,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ externalGroupOrgRoleMappingDAL, licenseService, permissionService, - orgRoleDAL + roleDAL }: TExternalGroupOrgRoleMappingServiceFactoryDep) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { const { permission } = await permissionService.getOrgPermission( @@ -61,7 +61,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ const mappings = await constructGroupOrgMembershipRoleMappings({ mappingsDTO: dto.mappings, - orgRoleDAL, + roleDAL, licenseService, orgId: actor.orgId }); diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index e801b607e..14885b1d6 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -47,14 +47,14 @@ export const externalMigrationServiceFactory = ({ throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } - const { membership } = await permissionService.getOrgPermission( + const { hasRole } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -94,7 +94,7 @@ export const externalMigrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TImportVaultDataDTO) => { - const { membership } = await permissionService.getOrgPermission( + const { hasRole } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -102,7 +102,7 @@ export const externalMigrationServiceFactory = ({ actorOrgId ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -150,7 +150,7 @@ export const externalMigrationServiceFactory = ({ actorAuthMethod, provider }: THasCustomVaultMigrationDTO) => { - const { membership } = await permissionService.getOrgPermission( + const { hasRole } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -158,7 +158,7 @@ export const externalMigrationServiceFactory = ({ actorOrgId ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); } diff --git a/backend/src/services/group-project/group-project-dal.ts b/backend/src/services/group-project/group-project-dal.ts index 263838a9d..4d14758dc 100644 --- a/backend/src/services/group-project/group-project-dal.ts +++ b/backend/src/services/group-project/group-project-dal.ts @@ -1,53 +1,44 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TMemberships, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, sqlNestRelationships } from "@app/lib/knex"; +import { sqlNestRelationships } from "@app/lib/knex"; export type TGroupProjectDALFactory = ReturnType; export const groupProjectDALFactory = (db: TDbClient) => { - const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); - const findByProjectId = async (projectId: string, filter?: { groupId?: string }, tx?: Knex) => { try { - const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) + const docs = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorGroupId`) .where((qb) => { if (filter?.groupId) { void qb.where(`${TableName.Groups}.id`, "=", filter.groupId); } }) - .join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.Groups, `${TableName.Membership}.actorGroupId`, `${TableName.Groups}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.GroupProjectMembership), - db.ref("createdAt").withSchema(TableName.GroupProjectMembership), - db.ref("updatedAt").withSchema(TableName.GroupProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), db.ref("id").as("groupId").withSchema(TableName.Groups), db.ref("name").as("groupName").withSchema(TableName.Groups), db.ref("slug").as("groupSlug").withSchema(TableName.Groups), - db.ref("id").withSchema(TableName.GroupProjectMembership), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole) + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole) ); const members = sqlNestRelationships({ @@ -104,8 +95,8 @@ export const groupProjectDALFactory = (db: TDbClient) => { try { const docs = await (tx || db.replicaNode())(TableName.UserGroupMembership) .where(`${TableName.UserGroupMembership}.userId`, userId) - .join(TableName.Groups, function () { - this.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn( + .join(TableName.Groups, (qb) => { + qb.on(`${TableName.UserGroupMembership}.groupId`, "=", `${TableName.Groups}.id`).andOn( `${TableName.Groups}.orgId`, "=", db.raw("?", [orgId]) @@ -131,32 +122,26 @@ export const groupProjectDALFactory = (db: TDbClient) => { const docs = await db(TableName.UserGroupMembership) // Join the GroupProjectMembership table with the Groups table to get the group name and slug. .join( - TableName.GroupProjectMembership, + TableName.Membership, `${TableName.UserGroupMembership}.groupId`, - `${TableName.GroupProjectMembership}.groupId` // this gives us access to the project id in the group membership + `${TableName.Membership}.actorGroupId` // this gives us access to the project id in the group membership ) - - .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) - - .where(`${TableName.GroupProjectMembership}.projectId`, projectId) - + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) - .join( - TableName.GroupProjectMembershipRole, - `${TableName.GroupProjectMembershipRole}.projectMembershipId`, - `${TableName.GroupProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.GroupProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .join(TableName.OrgMembership, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .join(db(TableName.Membership).as("orgMembership"), (qb) => { + qb.on(`${TableName.Users}.id`, `orgMembership.actorUserId`) + .andOn(`orgMembership.scope`, db.raw("?", [AccessScope.Organization])) + .andOn(`orgMembership.scopeOrgId`, `${TableName.Project}.orgId`); + }) .select( db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("createdAt").withSchema(TableName.UserGroupMembership), @@ -167,18 +152,18 @@ export const groupProjectDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.GroupProjectMembershipRole), - db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), - db.ref("isActive").withSchema(TableName.OrgMembership) + db.ref("isActive").withSchema("orgMembership") ) .where({ isGhost: false }); @@ -242,5 +227,5 @@ export const groupProjectDALFactory = (db: TDbClient) => { return members; }; - return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers }; + return { findByProjectId, findByUserId, findAllProjectGroupMembers }; }; diff --git a/backend/src/services/group-project/group-project-membership-role-dal.ts b/backend/src/services/group-project/group-project-membership-role-dal.ts deleted file mode 100644 index 5572ac6f5..000000000 --- a/backend/src/services/group-project/group-project-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TGroupProjectMembershipRoleDALFactory = ReturnType; - -export const groupProjectMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.GroupProjectMembershipRole); - return orm; -}; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 913aca2f3..4a4a61496 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,507 +1,27 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { ActionProjectType } from "@app/db/schemas"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; -import { ms } from "@app/lib/ms"; -import { isUuidV4 } from "@app/lib/validator"; +import { NotFoundError } from "@app/lib/errors"; import { TGroupDALFactory } from "../../ee/services/group/group-dal"; -import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; -import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; -import { TGroupProjectDALFactory } from "./group-project-dal"; -import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membership-role-dal"; -import { - TCreateProjectGroupDTO, - TDeleteProjectGroupDTO, - TGetGroupInProjectDTO, - TListProjectGroupDTO, - TUpdateProjectGroupDTO -} from "./group-project-types"; type TGroupProjectServiceFactoryDep = { - groupProjectDAL: Pick; - groupProjectMembershipRoleDAL: Pick< - TGroupProjectMembershipRoleDALFactory, - "create" | "transaction" | "insertMany" | "delete" - >; - userGroupMembershipDAL: Pick; - projectDAL: Pick; - projectKeyDAL: Pick; - projectRoleDAL: Pick; - projectBotDAL: TProjectBotDALFactory; groupDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" - >; + projectDAL: Pick; + permissionService: Pick; }; export type TGroupProjectServiceFactory = ReturnType; export const groupProjectServiceFactory = ({ groupDAL, - groupProjectDAL, - groupProjectMembershipRoleDAL, - userGroupMembershipDAL, projectDAL, - projectKeyDAL, - projectBotDAL, - projectRoleDAL, permissionService }: TGroupProjectServiceFactoryDep) => { - const addGroupToProject = async ({ - actor, - actorId, - actorOrgId, - actorAuthMethod, - roles, - projectId, - groupIdOrName - }: TCreateProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); - - let group: TGroups | null = null; - if (isUuidV4(groupIdOrName)) { - group = await groupDAL.findOne({ orgId: actorOrgId, id: groupIdOrName }); - } - if (!group) { - group = await groupDAL.findOne({ orgId: actorOrgId, name: groupIdOrName }); - } - - if (!group) throw new NotFoundError({ message: `Failed to find group with ID or name ${groupIdOrName}` }); - - const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (existingGroup) - throw new BadRequestError({ - message: `Group with ID ${group.id} already exists in project with id ${project.id}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - project.id - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign group to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId: project.id, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - - if (customRoles.length !== customInputRoles.length) { - const customRoleSlugs = customRoles.map((customRole) => customRole.slug); - const missingInputRoles = customInputRoles - .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) - .map((role) => role.role); - - throw new NotFoundError({ - message: `Custom role/s not found: ${missingInputRoles.join(", ")}` - }); - } - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const projectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupProjectMembership = await groupProjectDAL.create( - { - groupId: group!.id, - projectId: project.id - }, - tx - ); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: groupProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - - // share project key with users in group that have not - // individually been added to the project and that are not part of - // other groups that are in the project - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); - - if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) { - const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); - - if (!ghostUser) { - throw new NotFoundError({ - message: `Failed to find project owner of project with name ${project.name}` - }); - } - - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id, tx); - - if (!ghostUserLatestKey) { - throw new NotFoundError({ - message: `Failed to find project owner's latest key in project with name ${project.name}` - }); - } - - if (!ghostUserLatestKey.sender.publicKey) { - throw new NotFoundError({ - message: `Failed to find project owner's latest key in project with name ${project.name}` - }); - } - - const bot = await projectBotDAL.findOne({ projectId: project.id }, tx); - - if (!bot) { - throw new NotFoundError({ - message: `Failed to find project bot in project with name ${project.name}` - }); - } - - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({ - ciphertext: ghostUserLatestKey.encryptedKey, - nonce: ghostUserLatestKey.nonce, - publicKey: ghostUserLatestKey.sender.publicKey, - privateKey: botPrivateKey - }); - - const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { - if (!publicKey) { - throw new NotFoundError({ - message: `Failed to find user's public key in project with name ${project.name}` - }); - } - - const { ciphertext: encryptedKey, nonce } = crypto - .encryption() - .asymmetric() - .encrypt(plaintextProjectKey, publicKey, botPrivateKey); - - return { - encryptedKey, - nonce, - senderId: ghostUser.id, - receiverId: id, - projectId: project.id - }; - }); - - await projectKeyDAL.insertMany(projectKeyData, tx); - } - - return groupProjectMembership; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return projectGroup; - }; - - const updateGroupInProject = async ({ - projectId, - groupId, - roles, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TUpdateProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); - - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!projectGroup) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - project.id - ); - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign group to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.GrantPrivileges, - ProjectPermissionSub.Groups - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId: project.id, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) { - const customRoleSlugs = customRoles.map((customRole) => customRole.slug); - const missingInputRoles = customInputRoles - .filter((inputRole) => !customRoleSlugs.includes(inputRole.role)) - .map((role) => role.role); - - throw new NotFoundError({ - message: `Custom role/s not found: ${missingInputRoles.join(", ")}` - }); - } - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: projectGroup.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: projectGroup.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const updatedRoles = await groupProjectMembershipRoleDAL.transaction(async (tx) => { - await groupProjectMembershipRoleDAL.delete({ projectMembershipId: projectGroup.id }, tx); - return groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return updatedRoles; - }; - - const removeGroupFromProject = async ({ - projectId, - groupId, - actorId, - actor, - actorOrgId, - actorAuthMethod - }: TDeleteProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - - const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); - if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); - if (!groupProjectMembership) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); - - const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { - const groupMembersNotInProject = await userGroupMembershipDAL.findGroupMembersNotInProject( - group.id, - project.id, - tx - ); - - if (groupMembersNotInProject.length) { - await projectKeyDAL.delete( - { - projectId: project.id, - $in: { - receiverId: groupMembersNotInProject.map(({ user: { id } }) => id) - } - }, - tx - ); - } - - const [projectGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id }, tx); - return projectGroup; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return deletedProjectGroup; - }; - - const listGroupsInProject = async ({ - projectId, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TListProjectGroupDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) { - throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - - const groupMemberships = await groupProjectDAL.findByProjectId(project.id); - return groupMemberships; - }; - - const getGroupInProject = async ({ - actor, - actorId, - actorAuthMethod, - actorOrgId, - groupId, - projectId - }: TGetGroupInProjectDTO) => { - const project = await projectDAL.findById(projectId); - - if (!project) { - throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); - - const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { - groupId - }); - - if (!groupMembership) { - throw new NotFoundError({ - message: `Group membership with ID ${groupId} not found in project with ID ${projectId}` - }); - } - - return groupMembership; - }; - const listProjectGroupUsers = async ({ id, projectId, @@ -545,11 +65,6 @@ export const groupProjectServiceFactory = ({ }; return { - addGroupToProject, - updateGroupInProject, - removeGroupFromProject, - listGroupsInProject, - getGroupInProject, listProjectGroupUsers }; }; diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 71f7ec365..1b230f7a5 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -204,7 +204,7 @@ export const identityAccessTokenServiceFactory = ({ } const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId: identityAccessToken.identityId + actorIdentityId: identityAccessToken.identityId }); if (!identityOrgMembership) { @@ -219,7 +219,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; + return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId }; }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index af94c79c9..43584a1af 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -18,9 +18,10 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAliCloudAuthDALFactory } from "./identity-alicloud-auth-dal"; import { @@ -38,9 +39,10 @@ type TIdentityAliCloudAuthServiceFactoryDep = { TIdentityAliCloudAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; + orgDAL: Pick; }; export type TIdentityAliCloudAuthServiceFactory = ReturnType; @@ -48,9 +50,10 @@ export type TIdentityAliCloudAuthServiceFactory = ReturnType { const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); @@ -60,8 +63,9 @@ export const identityAliCloudAuthServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityAliCloudAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAliCloudAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); @@ -89,7 +93,7 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, @@ -150,7 +154,13 @@ export const identityAliCloudAuthServiceFactory = ({ }: TAttachAliCloudAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { @@ -166,13 +176,13 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -193,7 +203,7 @@ export const identityAliCloudAuthServiceFactory = ({ const identityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => { const doc = await identityAliCloudAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type: "iam", allowedArns, accessTokenMaxTTL, @@ -205,7 +215,7 @@ export const identityAliCloudAuthServiceFactory = ({ ); return doc; }); - return { ...identityAliCloudAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAliCloudAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateAliCloudAuth = async ({ @@ -220,7 +230,13 @@ export const identityAliCloudAuthServiceFactory = ({ actor, actorOrgId }: TUpdateAliCloudAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { @@ -242,13 +258,13 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -276,11 +292,17 @@ export const identityAliCloudAuthServiceFactory = ({ : undefined }); - return { ...updatedAliCloudAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedAliCloudAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getAliCloudAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAliCloudAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { @@ -294,12 +316,12 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityAliCloudAuth = async ({ @@ -309,17 +331,23 @@ export const identityAliCloudAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeAliCloudAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ message: "The identity does not have Alibaba Cloud auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -327,14 +355,15 @@ export const identityAliCloudAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -345,7 +374,7 @@ export const identityAliCloudAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke Alibaba Cloud auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -356,7 +385,7 @@ export const identityAliCloudAuthServiceFactory = ({ const deletedAliCloudAuth = await identityAliCloudAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.ALICLOUD_AUTH }, tx); - return { ...deletedAliCloudAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedAliCloudAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityAliCloudAuth; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 3dff47403..8793c3a00 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import axios from "axios"; import RE2 from "re2"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -17,9 +17,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal"; import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns"; @@ -36,9 +37,10 @@ import { type TIdentityAwsAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; + orgDAL: Pick; }; export type TIdentityAwsAuthServiceFactory = ReturnType; @@ -80,9 +82,10 @@ function isValidAwsRegion(region: string | null): boolean { export const identityAwsAuthServiceFactory = ({ identityAccessTokenDAL, identityAwsAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, licenseService, - permissionService + permissionService, + orgDAL }: TIdentityAwsAuthServiceFactoryDep) => { const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => { const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); @@ -90,7 +93,10 @@ export const identityAwsAuthServiceFactory = ({ throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAwsAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); @@ -153,7 +159,7 @@ export const identityAwsAuthServiceFactory = ({ } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, @@ -226,7 +232,13 @@ export const identityAwsAuthServiceFactory = ({ }: TAttachAwsAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { @@ -242,13 +254,13 @@ export const identityAwsAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -269,7 +281,7 @@ export const identityAwsAuthServiceFactory = ({ const identityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const doc = await identityAwsAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type: "iam", stsEndpoint, allowedPrincipalArns, @@ -283,7 +295,7 @@ export const identityAwsAuthServiceFactory = ({ ); return doc; }); - return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAwsAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateAwsAuth = async ({ @@ -300,7 +312,13 @@ export const identityAwsAuthServiceFactory = ({ actor, actorOrgId }: TUpdateAwsAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { @@ -321,13 +339,13 @@ export const identityAwsAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -357,11 +375,17 @@ export const identityAwsAuthServiceFactory = ({ : undefined }); - return { ...updatedAwsAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedAwsAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { @@ -375,12 +399,12 @@ export const identityAwsAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityAwsAuth = async ({ @@ -390,17 +414,23 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeAwsAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ message: "The identity does not have aws auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -408,14 +438,15 @@ export const identityAwsAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -426,7 +457,7 @@ export const identityAwsAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke aws auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -437,7 +468,7 @@ export const identityAwsAuthServiceFactory = ({ const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AWS_AUTH }, tx); - return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityAwsAuth; }; diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 9a2426a7c..b3250ed56 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,9 +14,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal"; import { validateAzureIdentity } from "./identity-azure-auth-fns"; @@ -33,20 +34,22 @@ type TIdentityAzureAuthServiceFactoryDep = { TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TIdentityAzureAuthServiceFactory = ReturnType; export const identityAzureAuthServiceFactory = ({ identityAzureAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + orgDAL }: TIdentityAzureAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => { const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); @@ -54,7 +57,10 @@ export const identityAzureAuthServiceFactory = ({ throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAzureAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAzureAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); const azureIdentity = await validateAzureIdentity({ @@ -80,7 +86,7 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, @@ -139,7 +145,13 @@ export const identityAzureAuthServiceFactory = ({ }: TAttachAzureAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { @@ -154,13 +166,13 @@ export const identityAzureAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -181,7 +193,7 @@ export const identityAzureAuthServiceFactory = ({ const identityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const doc = await identityAzureAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, tenantId, resource, allowedServicePrincipalIds, @@ -195,7 +207,7 @@ export const identityAzureAuthServiceFactory = ({ return doc; }); - return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateAzureAuth = async ({ @@ -212,7 +224,13 @@ export const identityAzureAuthServiceFactory = ({ actor, actorOrgId }: TUpdateAzureAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -232,13 +250,13 @@ export const identityAzureAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -270,12 +288,18 @@ export const identityAzureAuthServiceFactory = ({ return { ...updatedAzureAuth, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -288,13 +312,13 @@ export const identityAzureAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityAzureAuth = async ({ @@ -304,17 +328,23 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeAzureAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have azure auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -322,13 +352,14 @@ export const identityAzureAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -338,7 +369,7 @@ export const identityAzureAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke azure auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -349,7 +380,7 @@ export const identityAzureAuthServiceFactory = ({ const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.AZURE_AUTH }, tx); - return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityAzureAuth; }; diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 388c24d48..fe7b9b6d7 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,9 +14,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal"; import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns"; @@ -31,20 +32,22 @@ import { type TIdentityGcpAuthServiceFactoryDep = { identityGcpAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TIdentityGcpAuthServiceFactory = ReturnType; export const identityGcpAuthServiceFactory = ({ identityGcpAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, identityAccessTokenDAL, permissionService, - licenseService + licenseService, + orgDAL }: TIdentityGcpAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => { const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); @@ -52,7 +55,10 @@ export const identityGcpAuthServiceFactory = ({ throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityGcpAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) { throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); } @@ -119,7 +125,7 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, @@ -179,7 +185,13 @@ export const identityGcpAuthServiceFactory = ({ }: TAttachGcpAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -195,13 +207,13 @@ export const identityGcpAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -222,7 +234,7 @@ export const identityGcpAuthServiceFactory = ({ const identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const doc = await identityGcpAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type, allowedServiceAccounts, allowedProjects, @@ -236,7 +248,7 @@ export const identityGcpAuthServiceFactory = ({ ); return doc; }); - return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateGcpAuth = async ({ @@ -254,7 +266,13 @@ export const identityGcpAuthServiceFactory = ({ actor, actorOrgId }: TUpdateGcpAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -275,13 +293,13 @@ export const identityGcpAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -314,12 +332,18 @@ export const identityGcpAuthServiceFactory = ({ return { ...updatedGcpAuth, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -333,13 +357,13 @@ export const identityGcpAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityGcpAuth = async ({ @@ -349,7 +373,13 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeGcpAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { @@ -357,10 +387,10 @@ export const identityGcpAuthServiceFactory = ({ message: "The identity does not have gcp auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -368,13 +398,14 @@ export const identityGcpAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -384,7 +415,7 @@ export const identityGcpAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke gcp auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -395,7 +426,7 @@ export const identityGcpAuthServiceFactory = ({ const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.GCP_AUTH }, tx); - return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityGcpAuth; }; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 9cc851437..a99c8ad78 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -3,7 +3,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -24,11 +24,12 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal"; import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns"; @@ -43,22 +44,24 @@ import { type TIdentityJwtAuthServiceFactoryDep = { identityJwtAuthDAL: TIdentityJwtAuthDALFactory; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; + orgDAL: Pick; }; export type TIdentityJwtAuthServiceFactory = ReturnType; export const identityJwtAuthServiceFactory = ({ identityJwtAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, permissionService, licenseService, identityAccessTokenDAL, - kmsService + kmsService, + orgDAL }: TIdentityJwtAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); @@ -66,8 +69,9 @@ export const identityJwtAuthServiceFactory = ({ throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityJwtAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityJwtAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { throw new NotFoundError({ @@ -77,7 +81,7 @@ export const identityJwtAuthServiceFactory = ({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); @@ -207,7 +211,7 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, @@ -272,10 +276,14 @@ export const identityJwtAuthServiceFactory = ({ }: TAttachJwtAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); - if (!identityMembershipOrg) { - if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - } + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ message: "Failed to add JWT Auth to already configured identity" @@ -289,14 +297,14 @@ export const identityJwtAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -330,7 +338,7 @@ export const identityJwtAuthServiceFactory = ({ const identityJwtAuth = await identityJwtAuthDAL.transaction(async (tx) => { const doc = await identityJwtAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, configurationType, jwksUrl, encryptedJwksCaCert, @@ -349,7 +357,7 @@ export const identityJwtAuthServiceFactory = ({ return doc; }); - return { ...identityJwtAuth, orgId: identityMembershipOrg.orgId, jwksCaCert, publicKeys }; + return { ...identityJwtAuth, orgId: identityMembershipOrg.scopeOrgId, jwksCaCert, publicKeys }; }; const updateJwtAuth = async ({ @@ -371,7 +379,13 @@ export const identityJwtAuthServiceFactory = ({ actor, actorOrgId }: TUpdateJwtAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { @@ -392,14 +406,14 @@ export const identityJwtAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -462,14 +476,20 @@ export const identityJwtAuthServiceFactory = ({ return { ...updatedJwtAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, jwksCaCert: decryptedJwksCaCert, publicKeys: decryptedPublicKeys }; }; const getJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetJwtAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { @@ -481,7 +501,7 @@ export const identityJwtAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -502,14 +522,20 @@ export const identityJwtAuthServiceFactory = ({ return { ...identityJwtAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, jwksCaCert: decryptedJwksCaCert, publicKeys: decryptedPublicKeys }; }; const revokeJwtAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeJwtAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } @@ -520,10 +546,10 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -532,14 +558,15 @@ export const identityJwtAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -549,7 +576,7 @@ export const identityJwtAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke jwt auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -560,7 +587,7 @@ export const identityJwtAuthServiceFactory = ({ const deletedJwtAuth = await identityJwtAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.JWT_AUTH }, tx); - return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedJwtAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityJwtAuth; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index bc231c6d6..952b1e31d 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -3,7 +3,7 @@ import axios, { AxiosError } from "axios"; import https from "https"; import RE2 from "re2"; -import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; @@ -28,11 +28,12 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal"; import { extractK8sUsername } from "./identity-kubernetes-auth-fns"; @@ -52,7 +53,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -60,6 +61,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { gatewayV2Service: TGatewayV2ServiceFactory; gatewayDAL: Pick; gatewayV2DAL: Pick; + orgDAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -68,7 +70,7 @@ const GATEWAY_AUTH_DEFAULT_HOST = "https://kubernetes.default.svc.cluster.local" export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, identityAccessTokenDAL, permissionService, licenseService, @@ -76,7 +78,8 @@ export const identityKubernetesAuthServiceFactory = ({ gatewayV2Service, gatewayDAL, gatewayV2DAL, - kmsService + kmsService, + orgDAL }: TIdentityKubernetesAuthServiceFactoryDep) => { const $gatewayProxyWrapper = async ( inputs: { @@ -172,8 +175,9 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityKubernetesAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityKubernetesAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { throw new NotFoundError({ @@ -183,7 +187,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCert = ""; @@ -426,7 +430,7 @@ export const identityKubernetesAuthServiceFactory = ({ } const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, @@ -496,7 +500,13 @@ export const identityKubernetesAuthServiceFactory = ({ }: TAttachKubernetesAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { @@ -512,13 +522,13 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -538,8 +548,8 @@ export const identityKubernetesAuthServiceFactory = ({ let isGatewayV1 = true; if (gatewayId) { - const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` @@ -553,7 +563,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -565,13 +575,13 @@ export const identityKubernetesAuthServiceFactory = ({ const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const doc = await identityKubernetesAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, kubernetesHost, tokenReviewMode, allowedNamespaces, @@ -593,7 +603,7 @@ export const identityKubernetesAuthServiceFactory = ({ return doc; }); - return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; + return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.scopeOrgId }; }; const updateKubernetesAuth = async ({ @@ -615,7 +625,13 @@ export const identityKubernetesAuthServiceFactory = ({ actor, actorOrgId }: TUpdateKubernetesAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { @@ -637,13 +653,13 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -663,8 +679,8 @@ export const identityKubernetesAuthServiceFactory = ({ let isGatewayV1 = true; if (gatewayId) { - const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.scopeOrgId }); if (!gateway && !gatewayV2) { throw new NotFoundError({ @@ -679,7 +695,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -711,7 +727,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); if (caCert !== undefined) { @@ -742,7 +758,7 @@ export const identityKubernetesAuthServiceFactory = ({ return { ...updatedKubernetesAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, caCert: updatedCACert, tokenReviewerJwt: updatedTokenReviewerJwt }; @@ -755,7 +771,13 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetKubernetesAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); @@ -772,7 +794,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -780,7 +802,7 @@ export const identityKubernetesAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCert = ""; @@ -799,7 +821,7 @@ export const identityKubernetesAuthServiceFactory = ({ ...identityKubernetesAuth, caCert, tokenReviewerJwt, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, gatewayId: identityKubernetesAuth.gatewayId ?? identityKubernetesAuth.gatewayV2Id }; }; @@ -811,7 +833,13 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeKubernetesAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { @@ -819,10 +847,10 @@ export const identityKubernetesAuthServiceFactory = ({ message: "The identity does not have kubernetes auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -830,13 +858,14 @@ export const identityKubernetesAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -846,7 +875,7 @@ export const identityKubernetesAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke kubernetes auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -856,7 +885,7 @@ export const identityKubernetesAuthServiceFactory = ({ const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.KUBERNETES_AUTH }, tx); - return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityKubernetesAuth; }; diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 46ec2f98a..1a8ea3ed6 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -31,11 +31,12 @@ import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal"; import { @@ -55,7 +56,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TIdentityLdapAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: TKmsServiceFactory; @@ -65,6 +66,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" >; + orgDAL: Pick; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -78,18 +80,22 @@ export const identityLdapAuthServiceFactory = ({ identityAccessTokenDAL, identityDAL, identityLdapAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, licenseService, permissionService, kmsService, identityAuthTemplateDAL, - keyStore + keyStore, + orgDAL }: TIdentityLdapAuthServiceFactoryDep) => { const getLdapConfig = async (identityId: string) => { const identity = await identityDAL.findOne({ id: identityId }); if (!identity) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` }); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identity.id }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: identity.id, + scope: AccessScope.Organization + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` }); const ldapAuth = await identityLdapAuthDAL.findOne({ identityId: identity.id }); @@ -101,7 +107,7 @@ export const identityLdapAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityOrgMembership.orgId + orgId: identityOrgMembership.scopeOrgId }); const bindDN = decryptor({ cipherTextBlob: ldapAuth.encryptedBindDN }).toString(); @@ -112,7 +118,7 @@ export const identityLdapAuthServiceFactory = ({ const ldapConfig = { id: ldapAuth.id, - organization: identityOrgMembership.orgId, + organization: identityOrgMembership.scopeOrgId, url: ldapAuth.url, bindDN, bindPass, @@ -144,7 +150,10 @@ export const identityLdapAuthServiceFactory = ({ }; const login = async ({ identityId }: TLoginLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) { throw new UnauthorizedError({ @@ -160,7 +169,7 @@ export const identityLdapAuthServiceFactory = ({ }); } - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); if (!plan.ldap) { throw new BadRequestError({ message: @@ -169,7 +178,7 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, @@ -237,7 +246,13 @@ export const identityLdapAuthServiceFactory = ({ }: TAttachLdapAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -253,7 +268,7 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -266,7 +281,7 @@ export const identityLdapAuthServiceFactory = ({ ); } - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); if (!plan.ldap) { throw new BadRequestError({ @@ -296,11 +311,11 @@ export const identityLdapAuthServiceFactory = ({ const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const template = templateId - ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.scopeOrgId) : undefined; let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string }; @@ -354,7 +369,7 @@ export const identityLdapAuthServiceFactory = ({ const doc = await identityLdapAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, encryptedBindDN, encryptedBindPass, searchBase: ldapConfig.searchBase, @@ -376,7 +391,7 @@ export const identityLdapAuthServiceFactory = ({ ); return doc; }); - return { ...identityLdapAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityLdapAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateLdapAuth = async ({ @@ -402,7 +417,13 @@ export const identityLdapAuthServiceFactory = ({ lockoutDurationSeconds, lockoutCounterResetSeconds }: TUpdateLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -423,7 +444,7 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -436,7 +457,7 @@ export const identityLdapAuthServiceFactory = ({ ); } - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); if (!plan.ldap) { throw new BadRequestError({ @@ -465,11 +486,11 @@ export const identityLdapAuthServiceFactory = ({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const template = templateId - ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.scopeOrgId) : undefined; let config: { bindDN?: string; @@ -555,11 +576,17 @@ export const identityLdapAuthServiceFactory = ({ lockoutCounterResetSeconds }); - return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedLdapAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getLdapAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -573,14 +600,14 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const bindDN = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindDN }).toString(); @@ -590,7 +617,7 @@ export const identityLdapAuthServiceFactory = ({ : undefined; ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...ldapIdentityAuth, orgId: identityMembershipOrg.orgId, bindDN, bindPass, ldapCaCertificate }; + return { ...ldapIdentityAuth, orgId: identityMembershipOrg.scopeOrgId, bindDN, bindPass, ldapCaCertificate }; }; const revokeIdentityLdapAuth = async ({ @@ -600,17 +627,23 @@ export const identityLdapAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeLdapAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ message: "The identity does not have LDAP Auth attached" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -618,14 +651,15 @@ export const identityLdapAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -636,7 +670,7 @@ export const identityLdapAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke LDAP auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -647,7 +681,7 @@ export const identityLdapAuthServiceFactory = ({ const [deletedLdapAuth] = await identityLdapAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.LDAP_AUTH }, tx); - return { ...deletedLdapAuth, orgId: identityMembershipOrg.orgId }; + return { ...deletedLdapAuth, orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityLdapAuth; }; @@ -736,7 +770,13 @@ export const identityLdapAuthServiceFactory = ({ actorOrgId, actorAuthMethod }: TClearLdapAuthLockoutsDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { @@ -748,7 +788,7 @@ export const identityLdapAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -758,7 +798,7 @@ export const identityLdapAuthServiceFactory = ({ pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.LDAP_AUTH}:*` }); - return { deleted, identityId, orgId: identityMembershipOrg.orgId }; + return { deleted, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; return { diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index a4294250c..bfac3d158 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; import RE2 from "re2"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -19,9 +19,10 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal"; import { @@ -36,9 +37,10 @@ import { type TIdentityOciAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; identityOciAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; + orgDAL: Pick; }; export type TIdentityOciAuthServiceFactory = ReturnType; @@ -46,9 +48,10 @@ export type TIdentityOciAuthServiceFactory = ReturnType { const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); @@ -56,7 +59,10 @@ export const identityOciAuthServiceFactory = ({ throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityOciAuth.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. @@ -92,7 +98,7 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, @@ -154,7 +160,13 @@ export const identityOciAuthServiceFactory = ({ }: TAttachOciAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { @@ -170,13 +182,13 @@ export const identityOciAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -197,7 +209,7 @@ export const identityOciAuthServiceFactory = ({ const identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => { const doc = await identityOciAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, type: "iam", tenancyOcid, allowedUsernames, @@ -210,7 +222,7 @@ export const identityOciAuthServiceFactory = ({ ); return doc; }); - return { ...identityOciAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityOciAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateOciAuth = async ({ @@ -226,7 +238,13 @@ export const identityOciAuthServiceFactory = ({ actor, actorOrgId }: TUpdateOciAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { @@ -247,13 +265,13 @@ export const identityOciAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -282,11 +300,17 @@ export const identityOciAuthServiceFactory = ({ : undefined }); - return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedOciAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { @@ -300,12 +324,12 @@ export const identityOciAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...ociIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityOciAuth = async ({ @@ -315,17 +339,23 @@ export const identityOciAuthServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeOciAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ message: "The identity does not have OCI auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -333,14 +363,15 @@ export const identityOciAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -351,7 +382,7 @@ export const identityOciAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke OCI auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -362,7 +393,7 @@ export const identityOciAuthServiceFactory = ({ const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx); - return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityOciAuth; }; diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 6585e61f3..1218d8e1c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -4,7 +4,7 @@ import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; -import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -25,11 +25,12 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; @@ -43,22 +44,24 @@ import { type TIdentityOidcAuthServiceFactoryDep = { identityOidcAuthDAL: TIdentityOidcAuthDALFactory; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; + orgDAL: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; export const identityOidcAuthServiceFactory = ({ identityOidcAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, permissionService, licenseService, identityAccessTokenDAL, - kmsService + kmsService, + orgDAL }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -66,8 +69,9 @@ export const identityOidcAuthServiceFactory = ({ throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityOidcAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityOidcAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { throw new NotFoundError({ @@ -77,7 +81,7 @@ export const identityOidcAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCert = ""; @@ -178,7 +182,7 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, @@ -245,9 +249,15 @@ export const identityOidcAuthServiceFactory = ({ isActorSuperAdmin }: TAttachOidcAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) { - if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -262,14 +272,14 @@ export const identityOidcAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -289,13 +299,13 @@ export const identityOidcAuthServiceFactory = ({ const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const doc = await identityOidcAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, oidcDiscoveryUrl, encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob, boundIssuer, @@ -312,7 +322,7 @@ export const identityOidcAuthServiceFactory = ({ ); return doc; }); - return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; + return { ...identityOidcAuth, orgId: identityMembershipOrg.scopeOrgId, caCert }; }; const updateOidcAuth = async ({ @@ -333,7 +343,13 @@ export const identityOidcAuthServiceFactory = ({ actor, actorOrgId }: TUpdateOidcAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { @@ -354,14 +370,14 @@ export const identityOidcAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -396,7 +412,7 @@ export const identityOidcAuthServiceFactory = ({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); if (caCert !== undefined) { @@ -410,13 +426,19 @@ export const identityOidcAuthServiceFactory = ({ return { ...updatedOidcAuth, - orgId: identityMembershipOrg.orgId, + orgId: identityMembershipOrg.scopeOrgId, caCert: updatedCACert }; }; const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { @@ -428,7 +450,7 @@ export const identityOidcAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -438,18 +460,24 @@ export const identityOidcAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const caCert = identityOidcAuth.encryptedCaCertificate ? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString() : ""; - return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; + return { ...identityOidcAuth, orgId: identityMembershipOrg.scopeOrgId, caCert }; }; const revokeOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeOidcAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } @@ -460,10 +488,10 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -472,14 +500,15 @@ export const identityOidcAuthServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -490,7 +519,7 @@ export const identityOidcAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke oidc auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -501,7 +530,7 @@ export const identityOidcAuthServiceFactory = ({ const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OIDC_AUTH }, tx); - return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityOidcAuth; diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index e5e59607d..3dba6210d 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, TableName, TIdentities, TIdentityAlicloudAuths, @@ -15,7 +16,7 @@ import { TIdentityUniversalAuths } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types"; @@ -24,96 +25,82 @@ import { buildAuthMethods } from "../identity/identity-fns"; export type TIdentityProjectDALFactory = ReturnType; export const identityProjectDALFactory = (db: TDbClient) => { - const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership); - const findByIdentityId = async (identityId: string, tx?: Knex) => { try { - const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) - .where(`${TableName.IdentityProjectMembership}.identityId`, identityId) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectMembership}.id`, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` - ) - + const docs = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.actorIdentityId`, identityId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.IdentityUniversalAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTokenAuth}.identityId` ) .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity), db.ref("hasDeleteProtection").withSchema(TableName.Identity), - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("projectId").withSchema(TableName.IdentityProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), + db.ref("scopeProjectId").withSchema(TableName.Membership).as("projectId"), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), @@ -165,7 +152,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { }) }, project: { - id: projectId, + id: projectId as string, name: projectName, type: projectType } @@ -223,12 +210,10 @@ export const identityProjectDALFactory = (db: TDbClient) => { void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`); } }) - .join( - TableName.IdentityProjectMembership, - `${TableName.IdentityProjectMembership}.identityId`, - `${TableName.Identity}.id` - ) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) + .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .orderBy( `${TableName.Identity}.${filter.orderBy ?? ProjectIdentityOrderBy.Name}`, filter.orderDirection ?? OrderByDirection.ASC @@ -240,33 +225,20 @@ export const identityProjectDALFactory = (db: TDbClient) => { void fetchIdentitySubquery.offset(filter.offset ?? 0).limit(filter.limit); } - const query = (tx || db.replicaNode())(TableName.IdentityProjectMembership) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + const query = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .join(fetchIdentitySubquery, (bd) => { - bd.on(`${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`); + bd.on(`${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`); }) .where((qb) => { if (filter.identityId) { - void qb.where(`${TableName.IdentityProjectMembership}.identityId`, filter.identityId); + void qb.where(`${TableName.Membership}.actorIdentityId`, filter.identityId); } }) - .join( - TableName.IdentityProjectMembershipRole, - `${TableName.IdentityProjectMembershipRole}.projectMembershipId`, - `${TableName.IdentityProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.IdentityProjectMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) - .leftJoin( - TableName.IdentityProjectAdditionalPrivilege, - `${TableName.IdentityProjectMembership}.id`, - `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` - ) - + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.IdentityUniversalAuth, `${TableName.Identity}.id`, @@ -314,23 +286,23 @@ export const identityProjectDALFactory = (db: TDbClient) => { ) .select( - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), - db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity), - db.ref("id").withSchema(TableName.IdentityProjectMembership), - db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), + db.ref("id").withSchema(TableName.Membership), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -450,13 +422,14 @@ export const identityProjectDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const identities = await (tx || db.replicaNode())(TableName.IdentityProjectMembership) - .where(`${TableName.IdentityProjectMembership}.projectId`, projectId) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) + const identities = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .where((qb) => { if (filter.identityId) { - void qb.where("identityId", filter.identityId); + void qb.where(`${TableName.Membership}.actorIdentityId`, filter.identityId); } if (filter.search) { @@ -472,7 +445,6 @@ export const identityProjectDALFactory = (db: TDbClient) => { }; return { - ...identityProjectOrm, findByIdentityId, findByProjectId, getCountByProjectId diff --git a/backend/src/services/identity-project/identity-project-membership-role-dal.ts b/backend/src/services/identity-project/identity-project-membership-role-dal.ts deleted file mode 100644 index 3f6c6b589..000000000 --- a/backend/src/services/identity-project/identity-project-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TIdentityProjectMembershipRoleDALFactory = ReturnType; - -export const identityProjectMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.IdentityProjectMembershipRole); - return orm; -}; diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 08fd2cf0b..abe0446da 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,44 +1,22 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; -import { - constructPermissionErrorMessage, - validatePrivilegeChangeOperation -} from "@app/ee/services/permission/permission-fns"; +import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; -import { groupBy } from "@app/lib/fn"; -import { ms } from "@app/lib/ms"; +import { NotFoundError } from "@app/lib/errors"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; -import { TProjectDALFactory } from "../project/project-dal"; -import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TIdentityProjectDALFactory } from "./identity-project-dal"; -import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-membership-role-dal"; import { - TCreateProjectIdentityDTO, - TDeleteProjectIdentityDTO, TGetProjectIdentityByIdentityIdDTO, TGetProjectIdentityByMembershipIdDTO, - TListProjectIdentityDTO, - TUpdateProjectIdentityDTO + TListProjectIdentityDTO } from "./identity-project-types"; type TIdentityProjectServiceFactoryDep = { identityProjectDAL: TIdentityProjectDALFactory; - identityProjectMembershipRoleDAL: Pick< - TIdentityProjectMembershipRoleDALFactory, - "create" | "transaction" | "insertMany" | "delete" - >; - projectDAL: Pick; - projectRoleDAL: Pick; - identityOrgMembershipDAL: Pick; - permissionService: Pick< - TPermissionServiceFactory, - "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" - >; + permissionService: Pick; + membershipIdentityDAL: TMembershipIdentityDALFactory; }; export type TIdentityProjectServiceFactory = ReturnType; @@ -46,276 +24,8 @@ export type TIdentityProjectServiceFactory = ReturnType { - const createProjectIdentity = async ({ - identityId, - actor, - actorId, - actorOrgId, - actorAuthMethod, - projectId, - roles - }: TCreateProjectIdentityDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Create, - subject(ProjectPermissionSub.Identity, { - identityId - }) - ); - - const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId }); - if (existingIdentity) - throw new BadRequestError({ - message: `Identity with ID ${identityId} already exists in project with ID ${projectId}` - }); - - const project = await projectDAL.findById(projectId); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId, - orgId: project.orgId - }); - if (!identityOrgMembership) - throw new NotFoundError({ - message: `Failed to find identity with ID ${identityId}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - projectId - ); - - if (requestedRoleChange !== ProjectMembershipRole.NoAccess) { - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to assign to role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - details: { missingPermissions: permissionBoundary.missingPermissions } - }); - } - } - - // validate custom roles input - const customInputRoles = roles.filter( - ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) - throw new NotFoundError({ message: "One or more custom project roles not found" }); - - const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - const projectIdentity = await identityProjectDAL.transaction(async (tx) => { - const identityProjectMembership = await identityProjectDAL.create( - { - identityId, - projectId: project.id - }, - tx - ); - const sanitizedProjectMembershipRoles = roles.map((inputRole) => { - const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]); - if (!inputRole.isTemporary) { - return { - projectMembershipId: identityProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: identityProjectMembership.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - return { ...identityProjectMembership, roles: identityRoles }; - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return projectIdentity; - }; - - const updateProjectIdentity = async ({ - projectId, - identityId, - roles, - actor, - actorId, - actorAuthMethod, - actorOrgId - }: TUpdateProjectIdentityDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); - if (!projectIdentity) - throw new NotFoundError({ - message: `Identity with ID ${identityId} doesn't exists in project with ID ${projectId}` - }); - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - projectId - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity, - permission, - rolePermission - ); - - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - "Failed to change role", - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.GrantPrivileges, - ProjectPermissionSub.Identity - ), - 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) - ); - const hasCustomRole = Boolean(customInputRoles.length); - const customRoles = hasCustomRole - ? await projectRoleDAL.find({ - projectId, - $in: { slug: customInputRoles.map(({ role }) => role) } - }) - : []; - if (customRoles.length !== customInputRoles.length) - throw new NotFoundError({ message: "One or more custom project 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: projectIdentity.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null - }; - } - - // check cron or relative here later for now its just relative - const relativeTimeInMs = ms(inputRole.temporaryRange); - return { - projectMembershipId: projectIdentity.id, - role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role, - customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null, - isTemporary: true, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: inputRole.temporaryRange, - temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime), - temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs) - }; - }); - - const updatedRoles = await identityProjectMembershipRoleDAL.transaction(async (tx) => { - await identityProjectMembershipRoleDAL.delete({ projectMembershipId: projectIdentity.id }, tx); - return identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return updatedRoles; - }; - - const deleteProjectIdentity = async ({ - identityId, - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectId - }: TDeleteProjectIdentityDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); - if (!identityProjectMembership) { - throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - } - - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionIdentityActions.Delete, - subject(ProjectPermissionSub.Identity, { identityId }) - ); - - const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); - - await permissionService.invalidateProjectPermissionCache(projectId); - - return deletedIdentity; - }; - const listProjectIdentities = async ({ projectId, actor, @@ -391,9 +101,13 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetProjectIdentityByMembershipIdDTO) => { - const membership = await identityProjectDAL.findOne({ id: identityMembershipId }); + const membership = await membershipIdentityDAL.findOne({ + id: identityMembershipId, + scope: AccessScope.Project, + scopeOrgId: actorOrgId + }); - if (!membership) { + if (!membership || !membership.scopeProjectId || !membership.actorIdentityId) { throw new NotFoundError({ message: `Project membership with ID '${identityMembershipId}' not found` }); @@ -402,7 +116,7 @@ export const identityProjectServiceFactory = ({ const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId: membership.projectId, + projectId: membership.scopeProjectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.Any @@ -410,20 +124,17 @@ export const identityProjectServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionIdentityActions.Read, - subject(ProjectPermissionSub.Identity, { identityId: membership.identityId }) + subject(ProjectPermissionSub.Identity, { identityId: membership.actorIdentityId }) ); - const [identityMembership] = await identityProjectDAL.findByProjectId(membership.projectId, { - identityId: membership.identityId + const [identityMembership] = await identityProjectDAL.findByProjectId(membership.scopeProjectId, { + identityId: membership.actorIdentityId }); return identityMembership; }; return { - createProjectIdentity, - updateProjectIdentity, - deleteProjectIdentity, listProjectIdentities, getProjectIdentityByIdentityId, getProjectIdentityByMembershipId diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index ef2463eec..625b9b328 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -15,11 +15,11 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedEr import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; @@ -30,7 +30,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { TIdentityTlsCertAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: Pick; @@ -48,7 +48,7 @@ const parseSubjectDetails = (data: string) => { export const identityTlsCertAuthServiceFactory = ({ identityAccessTokenDAL, identityTlsCertAuthDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, licenseService, permissionService, kmsService @@ -61,8 +61,9 @@ export const identityTlsCertAuthServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ - identityId: identityTlsCertAuth.identityId + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityTlsCertAuth.identityId, + scope: AccessScope.Organization }); if (!identityMembershipOrg) { @@ -73,7 +74,7 @@ export const identityTlsCertAuthServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const caCertificate = decryptor({ @@ -118,7 +119,7 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, @@ -180,7 +181,13 @@ export const identityTlsCertAuthServiceFactory = ({ }) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { @@ -196,13 +203,13 @@ export const identityTlsCertAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -222,13 +229,13 @@ export const identityTlsCertAuthServiceFactory = ({ const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const identityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => { const doc = await identityTlsCertAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, accessTokenMaxTTL, allowedCommonNames, accessTokenTTL, @@ -240,7 +247,7 @@ export const identityTlsCertAuthServiceFactory = ({ ); return doc; }); - return { ...identityTlsCertAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityTlsCertAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateTlsCertAuth: TIdentityTlsCertAuthServiceFactory["updateTlsCertAuth"] = async ({ @@ -256,7 +263,13 @@ export const identityTlsCertAuthServiceFactory = ({ actor, actorOrgId }) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { @@ -278,13 +291,13 @@ export const identityTlsCertAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -303,7 +316,7 @@ export const identityTlsCertAuthServiceFactory = ({ }); const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); const updatedTlsCertAuth = await identityTlsCertAuthDAL.updateById(identityTlsCertAuth.id, { @@ -319,7 +332,7 @@ export const identityTlsCertAuthServiceFactory = ({ : undefined }); - return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getTlsCertAuth: TIdentityTlsCertAuthServiceFactory["getTlsCertAuth"] = async ({ @@ -329,7 +342,13 @@ export const identityTlsCertAuthServiceFactory = ({ actorAuthMethod, actorOrgId }) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { @@ -343,21 +362,21 @@ export const identityTlsCertAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }); let caCertificate = ""; if (identityAuth.encryptedCaCertificate) { caCertificate = decryptor({ cipherTextBlob: identityAuth.encryptedCaCertificate }).toString(); } - return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.orgId }; + return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeTlsCertAuth: TIdentityTlsCertAuthServiceFactory["revokeTlsCertAuth"] = async ({ @@ -367,32 +386,39 @@ export const identityTlsCertAuthServiceFactory = ({ actorAuthMethod, actorOrgId }) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ message: "The identity does not have TLS Certificate auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, memberships } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -403,7 +429,7 @@ export const identityTlsCertAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke TLS Certificate auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -414,7 +440,7 @@ export const identityTlsCertAuthServiceFactory = ({ const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx); await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx); - return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityTlsCertAuth; }; diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts index b7a08276b..eb9f4ab5d 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -1,4 +1,4 @@ -import { TIdentityAccessTokens, TIdentityOrgMemberships, TIdentityTlsCertAuths } from "@app/db/schemas"; +import { TIdentityAccessTokens, TIdentityTlsCertAuths, TMemberships } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; export type TLoginTlsCertAuthDTO = { @@ -40,7 +40,7 @@ export type TIdentityTlsCertAuthServiceFactory = { identityTlsCertAuth: TIdentityTlsCertAuths; accessToken: string; identityAccessToken: TIdentityAccessTokens; - identityMembershipOrg: TIdentityOrgMemberships; + identityMembershipOrg: TMemberships; }>; attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise; updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise; diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index d3743bd96..2ae05cb97 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod, TableName } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -14,9 +14,10 @@ import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/li import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal"; import { @@ -35,13 +36,14 @@ type TIdentityTokenAuthServiceFactoryDep = { TIdentityTokenAuthDALFactory, "transaction" | "create" | "findOne" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" >; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; }; export type TIdentityTokenAuthServiceFactory = ReturnType; @@ -49,10 +51,11 @@ export type TIdentityTokenAuthServiceFactory = ReturnType { const attachTokenAuth = async ({ identityId, @@ -68,7 +71,13 @@ export const identityTokenAuthServiceFactory = ({ }: TAttachTokenAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -84,13 +93,13 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -111,7 +120,7 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const doc = await identityTokenAuthDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, @@ -121,7 +130,7 @@ export const identityTokenAuthServiceFactory = ({ ); return doc; }); - return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const updateTokenAuth = async ({ @@ -138,7 +147,13 @@ export const identityTokenAuthServiceFactory = ({ }: TUpdateTokenAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -160,13 +175,13 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { if ( !plan.ipAllowlisting && @@ -195,12 +210,18 @@ export const identityTokenAuthServiceFactory = ({ return { ...updatedTokenAuth, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -214,13 +235,13 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; + return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityTokenAuth = async ({ @@ -233,7 +254,13 @@ export const identityTokenAuthServiceFactory = ({ }: TRevokeTokenAuthDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -244,22 +271,23 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -269,7 +297,7 @@ export const identityTokenAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke token auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -283,7 +311,7 @@ export const identityTokenAuthServiceFactory = ({ authMethod: IdentityAuthMethod.TOKEN_AUTH }); - return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityTokenAuth; }; @@ -299,7 +327,13 @@ export const identityTokenAuthServiceFactory = ({ }: TCreateTokenAuthTokenDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -310,22 +344,23 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -335,7 +370,7 @@ export const identityTokenAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create token for identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity ), @@ -345,7 +380,7 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, @@ -400,7 +435,13 @@ export const identityTokenAuthServiceFactory = ({ }: TGetTokenAuthTokensDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { @@ -411,7 +452,7 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -443,7 +484,13 @@ export const identityTokenAuthServiceFactory = ({ }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId: foundToken.identityId + }); if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); } @@ -457,21 +504,22 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -481,7 +529,7 @@ export const identityTokenAuthServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update token for identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity ), @@ -523,8 +571,9 @@ export const identityTokenAuthServiceFactory = ({ await validateIdentityUpdateForSuperAdminPrivileges(identityAccessToken.identityId, isActorSuperAdmin); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ - identityId: identityAccessToken.identityId + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: identityAccessToken.identityId, + scope: AccessScope.Organization }); if (!identityOrgMembership) { @@ -534,7 +583,7 @@ export const identityTokenAuthServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 979e6f25f..563a3f897 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -22,9 +22,10 @@ import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal"; import { TIdentityUaDALFactory } from "./identity-ua-dal"; @@ -44,9 +45,10 @@ type TIdentityUaServiceFactoryDep = { identityUaDAL: TIdentityUaDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; - identityOrgMembershipDAL: TIdentityOrgDALFactory; + membershipIdentityDAL: TMembershipIdentityDALFactory; permissionService: Pick; licenseService: Pick; + orgDAL: Pick; keyStore: Pick< TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" @@ -64,9 +66,10 @@ export const identityUaServiceFactory = ({ identityUaDAL, identityUaClientSecretDAL, identityAccessTokenDAL, - identityOrgMembershipDAL, + membershipIdentityDAL, permissionService, licenseService, + orgDAL, keyStore }: TIdentityUaServiceFactoryDep) => { const login = async (clientId: string, clientSecret: string, ip: string) => { @@ -97,7 +100,10 @@ export const identityUaServiceFactory = ({ }); } - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId }); + const identityMembershipOrg = await membershipIdentityDAL.findOne({ + actorIdentityId: identityUa.identityId, + scope: AccessScope.Organization + }); if (!identityMembershipOrg) { throw new UnauthorizedError({ message: "Invalid credentials" @@ -223,7 +229,7 @@ export const identityUaServiceFactory = ({ const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - await identityOrgMembershipDAL.updateById( + await membershipIdentityDAL.updateById( identityMembershipOrg.id, { lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, @@ -295,7 +301,13 @@ export const identityUaServiceFactory = ({ }: TAttachUaDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -311,13 +323,13 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { if ( !plan.ipAllowlisting && @@ -354,7 +366,7 @@ export const identityUaServiceFactory = ({ const identityUa = await identityUaDAL.transaction(async (tx) => { const doc = await identityUaDAL.create( { - identityId: identityMembershipOrg.identityId, + identityId: identityMembershipOrg.identity.id, clientId: crypto.nativeCrypto.randomUUID(), clientSecretTrustedIps: JSON.stringify(reformattedClientSecretTrustedIps), accessTokenMaxTTL, @@ -371,7 +383,7 @@ export const identityUaServiceFactory = ({ ); return doc; }); - return { ...identityUa, orgId: identityMembershipOrg.orgId }; + return { ...identityUa, orgId: identityMembershipOrg.scopeOrgId }; }; const updateUniversalAuth = async ({ @@ -391,7 +403,13 @@ export const identityUaServiceFactory = ({ lockoutDurationSeconds, lockoutCounterResetSeconds }: TUpdateUaDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); @@ -415,13 +433,13 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => { if ( !plan.ipAllowlisting && @@ -471,11 +489,17 @@ export const identityUaServiceFactory = ({ lockoutDurationSeconds, lockoutCounterResetSeconds }); - return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId }; + return { ...updatedUaAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); @@ -492,12 +516,12 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; + return { ...uaIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeIdentityUniversalAuth = async ({ @@ -507,7 +531,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId }: TRevokeUaDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -518,21 +548,22 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, membership } = await permissionService.getOrgPermission( + const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -542,7 +573,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke universal auth of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity ), @@ -551,7 +582,7 @@ export const identityUaServiceFactory = ({ const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); - return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId }; + return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.scopeOrgId }; }); return revokedIdentityUniversalAuth; }; @@ -566,7 +597,13 @@ export const identityUaServiceFactory = ({ description, numUsesLimit }: TCreateUaClientSecretDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -575,10 +612,10 @@ export const identityUaServiceFactory = ({ }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -586,13 +623,14 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -602,7 +640,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create client secret for identity.", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity ), @@ -613,7 +651,7 @@ export const identityUaServiceFactory = ({ const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecretHash = await crypto.hashing().createHash(clientSecret, appCfg.SALT_ROUNDS); - const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identityId }); + const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identity.id }); if (!identityUaAuth) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); const identityUaClientSecret = await identityUaClientSecretDAL.create({ @@ -629,7 +667,7 @@ export const identityUaServiceFactory = ({ return { clientSecret, clientSecretData: identityUaClientSecret, - orgId: identityMembershipOrg.orgId + orgId: identityMembershipOrg.scopeOrgId }; }; @@ -640,7 +678,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, identityId }: TGetUaClientSecretsDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -648,10 +692,10 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -659,14 +703,15 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity, permission, @@ -676,7 +721,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to get identity client secret with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity ), @@ -691,7 +736,7 @@ export const identityUaServiceFactory = ({ identityUAId: identityUniversalAuth.id, isClientSecretRevoked: false }); - return { clientSecrets, orgId: identityMembershipOrg.orgId }; + return { clientSecrets, orgId: identityMembershipOrg.scopeOrgId }; }; const getUniversalAuthClientSecretById = async ({ @@ -702,7 +747,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, clientSecretId }: TGetUniversalAuthClientSecretByIdDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -717,10 +768,10 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -728,13 +779,14 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity, permission, @@ -744,14 +796,14 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to read identity client secret of identity with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } }); - return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId }; + return { ...clientSecret, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; const revokeUniversalAuthClientSecret = async ({ @@ -762,7 +814,13 @@ export const identityUaServiceFactory = ({ actorAuthMethod, clientSecretId }: TRevokeUaClientSecretDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -777,10 +835,10 @@ export const identityUaServiceFactory = ({ const clientSecret = await identityUaClientSecretDAL.findOne({ id: clientSecretId, identityUAId: identityUa.id }); if (!clientSecret) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -788,14 +846,15 @@ export const identityUaServiceFactory = ({ const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, - identityMembershipOrg.identityId, - identityMembershipOrg.orgId, + identityMembershipOrg.identity.id, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity, permission, @@ -805,7 +864,7 @@ export const identityUaServiceFactory = ({ throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke identity client secret with more privileged role", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity ), @@ -817,7 +876,7 @@ export const identityUaServiceFactory = ({ isClientSecretRevoked: true }); - return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.orgId }; + return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; const clearUniversalAuthLockouts = async ({ @@ -827,7 +886,13 @@ export const identityUaServiceFactory = ({ actorOrgId, actorAuthMethod }: TClearUaLockoutsDTO) => { - const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + const identityMembershipOrg = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId + }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) { @@ -839,7 +904,7 @@ export const identityUaServiceFactory = ({ const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityMembershipOrg.orgId, + identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -849,7 +914,7 @@ export const identityUaServiceFactory = ({ pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:*` }); - return { deleted, identityId, orgId: identityMembershipOrg.orgId }; + return { deleted, identityId, orgId: identityMembershipOrg.scopeOrgId }; }; return { diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index c083df5aa..65aee561c 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, TableName, TIdentityAlicloudAuths, TIdentityAwsAuths, @@ -11,15 +12,15 @@ import { TIdentityKubernetesAuths, TIdentityOciAuths, TIdentityOidcAuths, - TIdentityOrgMemberships, TIdentityTlsCertAuths, TIdentityTokenAuths, TIdentityUniversalAuths, - TOrgRoles + TMembershipRoles, + TMemberships } from "@app/db/schemas"; import { TIdentityLdapAuths } from "@app/db/schemas/identity-ldap-auths"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; import { OrderByDirection } from "@app/lib/types"; import { @@ -33,80 +34,80 @@ import { buildAuthMethods } from "./identity-fns"; export type TIdentityOrgDALFactory = ReturnType; export const identityOrgDALFactory = (db: TDbClient) => { - const identityOrgOrm = ormify(db, TableName.IdentityOrgMembership); - - const findOne = async (filter: Partial, tx?: Knex) => { + const findOne = async (filter: Partial, tx?: Knex) => { try { - const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const [data] = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where((queryBuilder) => { Object.entries(filter).forEach(([key, value]) => { - void queryBuilder.where(`${TableName.IdentityOrgMembership}.${key}`, value); + void queryBuilder.where(`${TableName.Membership}.${key}`, value); }); }) - .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .leftJoin( TableName.IdentityUniversalAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTokenAuth}.identityId` ) .leftJoin( TableName.IdentityJwtAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityJwtAuth}.identityId` ) .leftJoin( TableName.IdentityLdapAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityLdapAuth}.identityId` ) .leftJoin( TableName.IdentityTlsCertAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTlsCertAuth}.identityId` ) .select( - selectAllTableCols(TableName.IdentityOrgMembership), + selectAllTableCols(TableName.Membership), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -129,7 +130,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { return { ...data, identity: { - id: data.identityId, + id: data.actorIdentityId as string, name, hasDeleteProtection, authMethods: buildAuthMethods(data) @@ -149,20 +150,18 @@ export const identityOrgDALFactory = (db: TDbClient) => { orderDirection = OrderByDirection.ASC, search, ...filter - }: Partial & + }: Partial & Pick, tx?: Knex ) => { try { const paginatedIdentity = (tx || db.replicaNode())(TableName.Identity) - .join( - TableName.IdentityOrgMembership, - `${TableName.IdentityOrgMembership}.identityId`, - `${TableName.Identity}.id` - ) + .join(TableName.Membership, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .select( - selectAllTableCols(TableName.IdentityOrgMembership), + selectAllTableCols(TableName.Membership), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity) ) @@ -181,84 +180,87 @@ export const identityOrgDALFactory = (db: TDbClient) => { type TSubquery = Awaited; const query = (tx || db.replicaNode()) .from(paginatedIdentity) - .leftJoin(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`) - + .join( + TableName.MembershipRole, + `${TableName.MembershipRole}.membershipId`, + "paginatedIdentity.id" + ) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`paginatedIdentity.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`paginatedIdentity.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) - .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( db.ref("id").withSchema("paginatedIdentity"), - db.ref("role").withSchema("paginatedIdentity"), - db.ref("roleId").withSchema("paginatedIdentity"), - db.ref("orgId").withSchema("paginatedIdentity"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("scopeOrgId").withSchema("paginatedIdentity").as("orgId"), db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"), db.ref("lastLoginTime").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), - db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), + db.ref("actorIdentityId").withSchema("paginatedIdentity").as("identityId"), db.ref("identityName").withSchema("paginatedIdentity"), db.ref("hasDeleteProtection").withSchema("paginatedIdentity"), @@ -276,12 +278,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth) ) // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) + .select(db.ref("id").as("crId").withSchema(TableName.Role)) + .select(db.ref("name").as("crName").withSchema(TableName.Role)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.Role)) + .select(db.ref("description").as("crDescription").withSchema(TableName.Role)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.Role)) .select( db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), @@ -327,7 +328,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { }) => ({ role, roleId, - identityId, + identityId: identityId as string, id, orgId, createdAt, @@ -344,7 +345,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { } : undefined, identity: { - id: identityId, + id: identityId as string, name: identityName, hasDeleteProtection, authMethods: buildAuthMethods({ @@ -394,20 +395,23 @@ export const identityOrgDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const searchQuery = (tx || db.replicaNode())(TableName.IdentityOrgMembership) - .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) - .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + const searchQuery = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .orderBy( orderBy === OrgIdentityOrderBy.Role - ? `${TableName.IdentityOrgMembership}.${orderBy}` + ? `${TableName.MembershipRole}.${orderBy}` : `${TableName.Identity}.${orderBy}`, orderDirection ) - .select(`${TableName.IdentityOrgMembership}.id`) + .select(`${TableName.Membership}.id`) .select<{ id: string; total_count: string }>( db.raw( - `count(${TableName.IdentityOrgMembership}."identityId") OVER(PARTITION BY ${TableName.IdentityOrgMembership}."orgId") as total_count` + `count(${TableName.Membership}."actorIdentityId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total_count` ) ) .as("searchedIdentities"); @@ -416,7 +420,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { buildKnexFilterForSearchResource(searchQuery, searchFilter, (attr) => { switch (attr) { case "role": - return [`${TableName.OrgRoles}.slug`, `${TableName.IdentityOrgMembership}.role`]; + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; case "name": return `${TableName.Identity}.name`; default: @@ -430,82 +434,85 @@ export const identityOrgDALFactory = (db: TDbClient) => { } type TSubquery = Awaited; - const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) - .where(`${TableName.IdentityOrgMembership}.orgId`, orgId) - .join(searchQuery, `${TableName.IdentityOrgMembership}.id`, "searchedIdentities.id") - .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) - .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + const query = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .join(searchQuery, `${TableName.Membership}.id`, "searchedIdentities.id") + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`) - .andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .leftJoin( TableName.IdentityUniversalAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityUniversalAuth}.identityId` ) .leftJoin( TableName.IdentityGcpAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityGcpAuth}.identityId` ) .leftJoin( TableName.IdentityAliCloudAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAliCloudAuth}.identityId` ) .leftJoin( TableName.IdentityAwsAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAwsAuth}.identityId` ) .leftJoin( TableName.IdentityKubernetesAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) .leftJoin( TableName.IdentityOciAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOciAuth}.identityId` ) .leftJoin( TableName.IdentityOidcAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityOidcAuth}.identityId` ) .leftJoin( TableName.IdentityAzureAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityAzureAuth}.identityId` ) .leftJoin( TableName.IdentityTokenAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityTokenAuth}.identityId` ) .leftJoin( TableName.IdentityJwtAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityJwtAuth}.identityId` ) .leftJoin( TableName.IdentityLdapAuth, - `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.Membership}.actorIdentityId`, `${TableName.IdentityLdapAuth}.identityId` ) .select( - db.ref("id").withSchema(TableName.IdentityOrgMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("total_count").withSchema("searchedIdentities"), - db.ref("role").withSchema(TableName.IdentityOrgMembership), - db.ref("roleId").withSchema(TableName.IdentityOrgMembership), - db.ref("orgId").withSchema(TableName.IdentityOrgMembership), - db.ref("createdAt").withSchema(TableName.IdentityOrgMembership), - db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), - db.ref("lastLoginAuthMethod").withSchema(TableName.IdentityOrgMembership), - db.ref("lastLoginTime").withSchema(TableName.IdentityOrgMembership), - db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("createdAt").withSchema(TableName.Membership), + db.ref("updatedAt").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), + db.ref("actorIdentityId").withSchema(TableName.Membership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity), @@ -522,12 +529,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth) ) // cr stands for custom role - .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) - .select(db.ref("name").as("crName").withSchema(TableName.OrgRoles)) - .select(db.ref("slug").as("crSlug").withSchema(TableName.OrgRoles)) - .select(db.ref("description").as("crDescription").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) - .select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles)) + .select(db.ref("id").as("crId").withSchema(TableName.Role)) + .select(db.ref("name").as("crName").withSchema(TableName.Role)) + .select(db.ref("slug").as("crSlug").withSchema(TableName.Role)) + .select(db.ref("description").as("crDescription").withSchema(TableName.Role)) + .select(db.ref("permissions").as("crPermission").withSchema(TableName.Role)) .select( db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), @@ -545,13 +551,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { ELSE ??.role END ? `, - [ - TableName.IdentityOrgMembership, - "custom", - TableName.OrgRoles, - TableName.IdentityOrgMembership, - db.raw(orderDirection) - ] + [TableName.MembershipRole, "custom", TableName.Role, TableName.MembershipRole, db.raw(orderDirection)] ); } @@ -590,7 +590,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { }) => ({ role, roleId, - identityId, + identityId: identityId as string, id, total_count: total_count as string, orgId, @@ -608,7 +608,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { } : undefined, identity: { - id: identityId, + id: identityId as string, name: identityName, hasDeleteProtection, authMethods: buildAuthMethods({ @@ -646,13 +646,15 @@ export const identityOrgDALFactory = (db: TDbClient) => { }; const countAllOrgIdentities = async ( - { search, ...filter }: Partial & Pick, + { search, ...filter }: Partial & Pick, tx?: Knex ) => { try { - const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) + const query = (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) .where(filter) - .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) + .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) .count(); if (search?.length) { @@ -667,5 +669,5 @@ export const identityOrgDALFactory = (db: TDbClient) => { } }; - return { ...identityOrgOrm, find, findOne, countAllOrgIdentities, searchIdentities }; + return { find, findOne, countAllOrgIdentities, searchIdentities }; }; diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index f216d6483..721844070 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas"; +import { AccessScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -12,6 +12,9 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityMetadataDALFactory } from "./identity-metadata-dal"; @@ -30,10 +33,13 @@ type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityMetadataDAL: TIdentityMetadataDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; identityProjectDAL: Pick; - permissionService: Pick; + permissionService: Pick; licenseService: Pick; keyStore: Pick; + orgDAL: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -45,7 +51,10 @@ export const identityServiceFactory = ({ identityProjectDAL, permissionService, licenseService, - keyStore + keyStore, + orgDAL, + membershipIdentityDAL, + membershipRoleDAL }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -58,33 +67,26 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission, membership } = await permissionService.getOrgPermission( - actor, - actorId, - orgId, - actorAuthMethod, - actorOrgId - ); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - role, - orgId - ); - const isCustomRole = Boolean(customRole); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], orgId); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); + const isCustomRole = Boolean(rolePermissionDetails?.role); if (role !== OrgMembershipRole.NoAccess) { const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity, permission, - rolePermission + rolePermissionDetails.permission ); if (!permissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create identity", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity ), @@ -103,12 +105,20 @@ export const identityServiceFactory = ({ const identity = await identityDAL.transaction(async (tx) => { const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx); - await identityOrgMembershipDAL.create( + const membership = await membershipIdentityDAL.create( { - identityId: newIdentity.id, - orgId, + scope: AccessScope.Organization, + actorIdentityId: newIdentity.id, + scopeOrgId: orgId + }, + tx + ); + + await membershipRoleDAL.create( + { + membershipId: membership.id, role: isCustomRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id + customRoleId: rolePermissionDetails?.role?.id }, tx ); @@ -155,45 +165,47 @@ export const identityServiceFactory = ({ }: TUpdateIdentityDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - let customRole: TOrgRoles | undefined; + let customRole: TRoles | undefined; if (role) { - const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole( - role, - identityOrgMembership.orgId - ); + const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], actorOrgId); + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); - const isCustomRole = Boolean(customOrgRole); + const isCustomRole = Boolean(rolePermissionDetails?.role); const appliedRolePermissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity, permission, - rolePermission + rolePermissionDetails?.permission ); if (!appliedRolePermissionBoundary.isValid) throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update identity", - membership.shouldUseNewPrivilegeSystem, + shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity ), details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } }); - if (isCustomRole) customRole = customOrgRole; + if (isCustomRole) customRole = rolePermissionDetails?.role; } const identity = await identityDAL.transaction(async (tx) => { @@ -203,11 +215,12 @@ export const identityServiceFactory = ({ : await identityDAL.findById(id, tx); if (role) { - await identityOrgMembershipDAL.updateById( - identityOrgMembership.id, + await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx); + await membershipRoleDAL.create( { + membershipId: identityOrgMembership.id, role: customRole ? OrgMembershipRole.Custom : role, - roleId: customRole?.id || null + customRoleId: customRole?.id || null }, tx ); @@ -219,12 +232,12 @@ export const identityServiceFactory = ({ }> = []; if (metadata) { - await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx); + await identityMetadataDAL.delete({ orgId: identityOrgMembership.scopeOrgId, identityId: id }, tx); if (metadata.length) { const rowsToInsert = metadata.map(({ key, value }) => ({ identityId: newIdentity.id, - orgId: identityOrgMembership.orgId, + orgId: identityOrgMembership.scopeOrgId, key, value })); @@ -239,12 +252,14 @@ export const identityServiceFactory = ({ }; }); - return { ...identity, orgId: identityOrgMembership.orgId }; + return { ...identity, orgId: identityOrgMembership.scopeOrgId }; }; const getIdentityById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetIdentityByIdDTO) => { const doc = await identityOrgMembershipDAL.find({ - [`${TableName.IdentityOrgMembership}.identityId` as "identityId"]: id + [`${TableName.Membership}.actorIdentityId` as "actorIdentityId"]: id, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId }); const identity = doc[0]; if (!identity) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); @@ -258,6 +273,7 @@ export const identityServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + // TODO(namespace): check this in identity service const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockoutAuthMethods = new Set(); @@ -289,14 +305,19 @@ export const identityServiceFactory = ({ isActorSuperAdmin }: TDeleteIdentityDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(id, isActorSuperAdmin); - - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); + const identityOrgMembership = await membershipIdentityDAL.getIdentityById({ + scopeData: { + scope: AccessScope.Organization, + orgId: actorOrgId + }, + identityId: id + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); @@ -308,9 +329,9 @@ export const identityServiceFactory = ({ const deletedIdentity = await identityDAL.deleteById(id); - await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); - return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; + return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; }; const listOrgIdentities = async ({ @@ -329,7 +350,8 @@ export const identityServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ - [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + scope: AccessScope.Organization, limit, offset, orderBy, @@ -338,7 +360,7 @@ export const identityServiceFactory = ({ }); const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({ - [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, search }); @@ -379,13 +401,17 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId }: TListProjectIdentitiesByIdentityIdDTO) => { - const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: identityId, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getOrgPermission( actor, actorId, - identityOrgMembership.orgId, + identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId ); diff --git a/backend/src/services/membership-group/membership-group-dal.ts b/backend/src/services/membership-group/membership-group-dal.ts new file mode 100644 index 000000000..b3c35e46f --- /dev/null +++ b/backend/src/services/membership-group/membership-group-dal.ts @@ -0,0 +1,272 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; + +export type TMembershipGroupDALFactory = ReturnType; + +type TFindGroupArg = { + scopeData: AccessScopeData; + tx?: Knex; + filter: Partial<{ + limit: number; + offset: number; + groupId: string; + name: Omit; + role: Omit; + }>; +}; + +type TGetGroupByIdArg = { + scopeData: AccessScopeData; + tx?: Knex; + groupId: string; +}; + +export const membershipGroupDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + + const getGroupById = async ({ scopeData, tx, groupId }: TGetGroupByIdArg) => { + try { + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Membership}.actorGroupId`, groupId) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("name").withSchema(TableName.Role).as("roleName"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt") + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { groupName, groupSlug } = el; + return { + ...MembershipsSchema.parse(el), + group: { + id: groupId, + name: groupName, + slug: groupSlug + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + roleName, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + customRoleName: roleName, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipGetByGroupId" }); + } + }; + + const findGroups = async ({ scopeData, tx, filter }: TFindGroupArg) => { + try { + const paginatedGroups = (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where((qb) => { + if (filter.groupId) { + void qb.where(`${TableName.Groups}.id`, filter.groupId); + } + + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }); + + if (filter.limit) void paginatedGroups.limit(filter.limit); + if (filter.offset) void paginatedGroups.offset(filter.offset); + + if (filter.name || filter.role) { + buildKnexFilterForSearchResource( + paginatedGroups, + { + name: filter.name!, + role: filter.role! + }, + (attr) => { + switch (attr) { + case "role": + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; + case "name": + return `${TableName.Groups}.name`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + } + ); + } + + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorGroupId`) + .join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .whereIn(`${TableName.Membership}.id`, paginatedGroups) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Groups).as("groupName"), + db.ref("slug").withSchema(TableName.Groups).as("groupSlug"), + db.ref("id").withSchema(TableName.Groups).as("groupId"), + + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("name").withSchema(TableName.Role).as("roleName"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt") + ) + .select( + db.raw( + `count(${TableName.Membership}."actorGroupId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total` + ) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { groupId, groupName, groupSlug } = el; + return { + ...MembershipsSchema.parse(el), + group: { + id: groupId, + name: groupName, + slug: groupSlug + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + roleName, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + customRoleName: roleName, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipfindGroup" }); + } + }; + + return { ...orm, findGroups, getGroupById }; +}; diff --git a/backend/src/services/membership-group/membership-group-service.ts b/backend/src/services/membership-group/membership-group-service.ts new file mode 100644 index 000000000..18f6b3ad1 --- /dev/null +++ b/backend/src/services/membership-group/membership-group-service.ts @@ -0,0 +1,325 @@ +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 { + TCreateMembershipGroupDTO, + TDeleteMembershipGroupDTO, + TGetMembershipGroupByGroupIdDTO, + TListMembershipGroupDTO, + 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; + +export const membershipGroupServiceFactory = ({ + membershipGroupDAL, + roleDAL, + membershipRoleDAL, + orgDAL, + permissionService +}: TMembershipGroupServiceFactoryDep) => { + 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; + const factory = scopeFactory[scopeData.scope]; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Group must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + await factory.onCreateMembershipGroupGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membership = await membershipGroupDAL.transaction(async (tx) => { + const doc = await membershipGroupDAL.create( + { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.data.groupId + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.insertMany(roleDocs, tx); + return doc; + }); + + return { membership }; + }; + + const updateMembership = async (dto: TUpdateMembershipGroupDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateMembershipGroupGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Group must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipGroupDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.selector.groupId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Group doesn't have membership" + }); + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipGroupDAL.transaction(async (tx) => { + const doc = + typeof data?.isActive === "undefined" + ? existingMembership + : await membershipGroupDAL.updateById( + existingMembership.id, + { + isActive: data.isActive + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.delete( + { + membershipId: doc.id + }, + tx + ); + const roles = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles }; + }); + + return { membership: membershipDoc }; + }; + + const deleteMembership = async (dto: TDeleteMembershipGroupDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteMembershipGroupGuard(dto); + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipGroupDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorGroupId: dto.selector.groupId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Group doesn't have membership" + }); + + if (existingMembership.actorGroupId === dto.permission.id) + throw new BadRequestError({ + message: "You can't delete your own membership" + }); + + const membershipDoc = await membershipGroupDAL.transaction(async (tx) => { + await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); + const doc = await membershipGroupDAL.deleteById(existingMembership.id, tx); + return doc; + }); + return { membership: membershipDoc }; + }; + + const listMemberships = async (dto: TListMembershipGroupDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipGroupGuard(dto); + const memberships = await membershipGroupDAL.findGroups({ + scopeData, + filter: { + limit: dto.data.limit, + offset: dto.data.offset, + name: dto.data.groupName + ? { + [SearchResourceOperators.$contains]: dto.data.groupName + } + : undefined, + role: dto.data.roles?.length + ? { + [SearchResourceOperators.$in]: dto.data.roles + } + : undefined + } + }); + return { memberships: memberships.data, totalCount: memberships.totalCount }; + }; + + const getMembershipByGroupId = async (dto: TGetMembershipGroupByGroupIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetMembershipGroupByGroupIdGuard(dto); + const membership = await membershipGroupDAL.getGroupById({ + scopeData, + groupId: selector.groupId + }); + if (!membership) throw new NotFoundError({ message: `Group membership not found` }); + + return { membership }; + }; + + return { + createMembership, + updateMembership, + deleteMembership, + listMemberships, + getMembershipByGroupId + }; +}; diff --git a/backend/src/services/membership-group/membership-group-types.ts b/backend/src/services/membership-group/membership-group-types.ts new file mode 100644 index 000000000..19c374458 --- /dev/null +++ b/backend/src/services/membership-group/membership-group-types.ts @@ -0,0 +1,79 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TMembershipGroupScopeFactory { + onCreateMembershipGroupGuard: (arg: TCreateMembershipGroupDTO) => Promise; + + onUpdateMembershipGroupGuard: (arg: TUpdateMembershipGroupDTO) => Promise; + onDeleteMembershipGroupGuard: (arg: TDeleteMembershipGroupDTO) => Promise; + onListMembershipGroupGuard: (arg: TListMembershipGroupDTO) => Promise; + onGetMembershipGroupByGroupIdGuard: (arg: TGetMembershipGroupByGroupIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + getScopeDatabaseFields: (scope: AccessScopeData) => { + scopeOrgId: string; + scopeNamespaceId?: string | null; + scopeProjectId?: string | null; + }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + groupId: string; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TUpdateMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + groupId: string; + }; + data: { + isActive?: boolean; + metadata?: { key: string; value: string }[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TListMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + limit?: number; + offset?: number; + groupName?: string; + roles?: string[]; + }; +}; + +export type TDeleteMembershipGroupDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + groupId: string; + }; +}; + +export type TGetMembershipGroupByGroupIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + groupId: string; + }; +}; diff --git a/backend/src/services/membership-group/namespace/namespace-membership-group-factory.ts b/backend/src/services/membership-group/namespace/namespace-membership-group-factory.ts new file mode 100644 index 000000000..5c8973b1b --- /dev/null +++ b/backend/src/services/membership-group/namespace/namespace-membership-group-factory.ts @@ -0,0 +1,61 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TMembershipGroupScopeFactory } from "../membership-group-types"; + +type TNamespaceMembershipGroupScopeFactoryDep = Record; + +export const newNamespaceMembershipGroupFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceMembershipGroupScopeFactoryDep +): TMembershipGroupScopeFactory => { + const getScopeField: TMembershipGroupScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const getScopeDatabaseFields: TMembershipGroupScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const isCustomRole: TMembershipGroupScopeFactory["isCustomRole"] = () => { + throw new InternalServerError({ message: "Namespace membership group isCustomRole not implemented" }); + }; + + const onCreateMembershipGroupGuard: TMembershipGroupScopeFactory["onCreateMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group create not implemented" }); + }; + + const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group update not implemented" }); + }; + + const onDeleteMembershipGroupGuard: TMembershipGroupScopeFactory["onDeleteMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group delete not implemented" }); + }; + + const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership group list not implemented" }); + }; + + const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership group get by group id not implemented" }); + }; + + return { + onCreateMembershipGroupGuard, + onUpdateMembershipGroupGuard, + onDeleteMembershipGroupGuard, + onListMembershipGroupGuard, + onGetMembershipGroupByGroupIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-group/org/org-membership-group-factory.ts b/backend/src/services/membership-group/org/org-membership-group-factory.ts new file mode 100644 index 000000000..1e87ee3ca --- /dev/null +++ b/backend/src/services/membership-group/org/org-membership-group-factory.ts @@ -0,0 +1,125 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { OrgPermissionGroupActions, 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 { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TMembershipGroupScopeFactory } from "../membership-group-types"; + +type TOrgMembershipGroupScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; +}; + +export const newOrgMembershipGroupFactory = ({ + permissionService, + orgDAL +}: TOrgMembershipGroupScopeFactoryDep): TMembershipGroupScopeFactory => { + const getScopeField: TMembershipGroupScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const getScopeDatabaseFields: TMembershipGroupScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { scopeOrgId: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const isCustomRole: TMembershipGroupScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateMembershipGroupGuard: TMembershipGroupScopeFactory["onCreateMembershipGroupGuard"] = async () => { + throw new BadRequestError({ + message: "Organization membership cannot be created for groups" + }); + }; + + const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); + const permissionRoles = await permissionService.getOrgPermissionByRoles( + dto.data.roles.map((el) => el.role), + dto.permission.orgId + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create group org membership", + shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.GrantPrivileges, + OrgPermissionSubjects.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onDeleteMembershipGroupGuard: TMembershipGroupScopeFactory["onDeleteMembershipGroupGuard"] = async () => { + throw new BadRequestError({ + message: "Organization membership cannot be deleted for organization scoped group" + }); + }; + + const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); + }; + + const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); + }; + + return { + onCreateMembershipGroupGuard, + onUpdateMembershipGroupGuard, + onDeleteMembershipGroupGuard, + onListMembershipGroupGuard, + onGetMembershipGroupByGroupIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-group/project/project-membership-group-factory.ts b/backend/src/services/membership-group/project/project-membership-group-factory.ts new file mode 100644 index 000000000..5a63eed1e --- /dev/null +++ b/backend/src/services/membership-group/project/project-membership-group-factory.ts @@ -0,0 +1,188 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionGroupActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TMembershipGroupDALFactory } from "../membership-group-dal"; +import { TMembershipGroupScopeFactory } from "../membership-group-types"; + +type TProjectMembershipGroupScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipGroupDAL: Pick; +}; + +export const newProjectMembershipGroupFactory = ({ + permissionService, + orgDAL, + membershipGroupDAL +}: TProjectMembershipGroupScopeFactoryDep): TMembershipGroupScopeFactory => { + const getScopeField: TMembershipGroupScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const getScopeDatabaseFields: TMembershipGroupScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const isCustomRole: TMembershipGroupScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role); + + const onCreateMembershipGroupGuard: TMembershipGroupScopeFactory["onCreateMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); + const orgMembership = await membershipGroupDAL.findOne({ + actorGroupId: dto.data.groupId, + scopeOrgId: dto.permission.orgId, + scope: AccessScope.Organization + }); + if (!orgMembership) + throw new BadRequestError({ message: `Group ${dto.data.groupId} is missing organization membership` }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create group project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onUpdateMembershipGroupGuard: TMembershipGroupScopeFactory["onUpdateMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update group project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.GrantPrivileges, + ProjectPermissionSub.Groups + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onDeleteMembershipGroupGuard: TMembershipGroupScopeFactory["onDeleteMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); + }; + + const onListMembershipGroupGuard: TMembershipGroupScopeFactory["onListMembershipGroupGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + }; + + const onGetMembershipGroupByGroupIdGuard: TMembershipGroupScopeFactory["onGetMembershipGroupByGroupIdGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + }; + + return { + onCreateMembershipGroupGuard, + onUpdateMembershipGroupGuard, + onDeleteMembershipGroupGuard, + onListMembershipGroupGuard, + onGetMembershipGroupByGroupIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts new file mode 100644 index 000000000..64e508fb8 --- /dev/null +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -0,0 +1,357 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; + +import { buildAuthMethods } from "../identity/identity-fns"; + +export type TMembershipIdentityDALFactory = ReturnType; + +type TFindIdentityArg = { + scopeData: AccessScopeData; + tx?: Knex; + filter: Partial<{ + limit: number; + offset: number; + identityId: string; + name: Omit; + role: Omit; + }>; +}; + +type TGetIdentityByIdArg = { + scopeData: AccessScopeData; + tx?: Knex; + identityId: string; +}; + +export const membershipIdentityDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + + const getIdentityById = async ({ scopeData, tx, identityId }: TGetIdentityByIdArg) => { + try { + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Membership}.actorIdentityId`, identityId) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .leftJoin( + TableName.IdentityUniversalAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) + .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`) + .leftJoin( + TableName.IdentityKubernetesAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`) + .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) + .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`) + .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) + .leftJoin( + TableName.IdentityTlsCertAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityTlsCertAuth}.identityId` + ) + .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`) + .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), + + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), + db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), + db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), + db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), + db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), + db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), + db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), + db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { + identityId: actorIdentityId, + identityHasDeleteProtection, + identityName, + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + alicloudId, + tokenId, + jwtId, + ociId, + ldapId, + tlsCertId + } = el; + return { + ...MembershipsSchema.parse(el), + identity: { + name: identityName, + id: actorIdentityId, + hasDeleteProtection: identityHasDeleteProtection, + authMethods: buildAuthMethods({ + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + tokenId, + alicloudId, + jwtId, + ldapId, + ociId, + tlsCertId + }) + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipGetByIdentityId" }); + } + }; + + const findIdentities = async ({ scopeData, tx, filter }: TFindIdentityArg) => { + try { + const paginatedIdentitys = (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where((qb) => { + if (filter.identityId) { + void qb.where(`${TableName.Identity}.id`, filter.identityId); + } + + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }); + + if (filter.limit) void paginatedIdentitys.limit(filter.limit); + if (filter.offset) void paginatedIdentitys.offset(filter.offset); + + if (filter.name || filter.role) { + buildKnexFilterForSearchResource( + paginatedIdentitys, + { + name: filter.name!, + role: filter.role! + }, + (attr) => { + switch (attr) { + case "role": + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; + case "name": + return `${TableName.Identity}.name`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + } + ); + } + + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .whereIn(`${TableName.Membership}.id`, paginatedIdentitys) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("name").withSchema(TableName.Identity).as("identityName"), + db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), + + db.ref("slug").withSchema(TableName.Role).as("roleSlug"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt") + ) + .select( + db.raw( + `count(${TableName.Membership}."actorIdentityId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total` + ) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => { + const { identityId: actorIdentityId, identityHasDeleteProtection, identityName } = el; + return { + ...MembershipsSchema.parse(el), + identity: { + name: identityName, + id: actorIdentityId, + hasDeleteProtection: identityHasDeleteProtection + } + }; + }, + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + roleSlug, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug: roleSlug, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipfindIdentity" }); + } + }; + + return { ...orm, findIdentities, getIdentityById }; +}; diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts new file mode 100644 index 000000000..4dd3da064 --- /dev/null +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -0,0 +1,339 @@ +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 { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TRoleDALFactory } from "../role/role-dal"; +import { TMembershipIdentityDALFactory } from "./membership-identity-dal"; +import { + TCreateMembershipIdentityDTO, + TDeleteMembershipIdentityDTO, + TGetMembershipIdentityByIdentityIdDTO, + TListMembershipIdentityDTO, + TUpdateMembershipIdentityDTO +} from "./membership-identity-types"; +import { newNamespaceMembershipIdentityFactory } from "./namespace/namespace-membership-identity-factory"; +import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory"; +import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory"; + +type TMembershipIdentityServiceFactoryDep = { + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: Pick; + roleDAL: Pick; + permissionService: Pick< + TPermissionServiceFactory, + "getOrgPermission" | "getProjectPermission" | "getProjectPermissionByRoles" | "getOrgPermissionByRoles" + >; + orgDAL: Pick; + additionalPrivilegeDAL: Pick; +}; + +export type TMembershipIdentityServiceFactory = ReturnType; + +export const membershipIdentityServiceFactory = ({ + membershipIdentityDAL, + roleDAL, + membershipRoleDAL, + permissionService, + orgDAL, + additionalPrivilegeDAL +}: TMembershipIdentityServiceFactoryDep) => { + const scopeFactory = { + [AccessScope.Organization]: newOrgMembershipIdentityFactory({ + orgDAL, + permissionService + }), + [AccessScope.Project]: newProjectMembershipIdentityFactory({ + membershipIdentityDAL, + orgDAL, + permissionService + }), + [AccessScope.Namespace]: newNamespaceMembershipIdentityFactory({}) + }; + + const createMembership = async (dto: TCreateMembershipIdentityDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Identity must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + await factory.onCreateMembershipIdentityGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membership = await membershipIdentityDAL.transaction(async (tx) => { + const doc = await membershipIdentityDAL.create( + { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.data.identityId + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.insertMany(roleDocs, tx); + return doc; + }); + + return { membership }; + }; + + const updateMembership = async (dto: TUpdateMembershipIdentityDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateMembershipIdentityGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "Identity must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipIdentityDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.selector.identityId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Identity doesn't have membership" + }); + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipIdentityDAL.transaction(async (tx) => { + const doc = + typeof data.isActive === "undefined" + ? existingMembership + : await membershipIdentityDAL.updateById( + existingMembership.id, + { + isActive: data.isActive + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.delete( + { + membershipId: doc.id + }, + tx + ); + const insertedRoleDocs = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles: insertedRoleDocs }; + }); + + return { membership: membershipDoc }; + }; + + const deleteMembership = async (dto: TDeleteMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteMembershipIdentityGuard(dto); + + const scopeField = factory.getScopeField(scopeData); + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipIdentityDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorIdentityId: dto.selector.identityId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "Identity doesn't have membership" + }); + + if (existingMembership.actorIdentityId === dto.permission.id) + throw new BadRequestError({ + message: "You can't delete your own membership" + }); + + const membershipDoc = await membershipIdentityDAL.transaction(async (tx) => { + await additionalPrivilegeDAL.delete( + { + actorIdentityId: dto.selector.identityId, + [scopeField.key]: scopeField.value + }, + tx + ); + await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); + const doc = await membershipIdentityDAL.deleteById(existingMembership.id, tx); + return doc; + }); + return { membership: membershipDoc }; + }; + + const listMemberships = async (dto: TListMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipIdentityGuard(dto); + const memberships = await membershipIdentityDAL.findIdentities({ + scopeData, + filter: { + limit: dto.data.limit, + offset: dto.data.offset, + name: dto.data.identityName + ? { + [SearchResourceOperators.$contains]: dto.data.identityName + } + : undefined, + role: dto.data.roles.length + ? { + [SearchResourceOperators.$in]: dto.data.roles + } + : undefined + } + }); + return memberships; + }; + + const getMembershipByIdentityId = async (dto: TGetMembershipIdentityByIdentityIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetMembershipIdentityByIdentityIdGuard(dto); + const membership = await membershipIdentityDAL.getIdentityById({ + scopeData, + identityId: selector.identityId + }); + if (!membership) throw new NotFoundError({ message: `Identity membership not found` }); + + return membership; + }; + + return { + createMembership, + updateMembership, + deleteMembership, + listMemberships, + getMembershipByIdentityId + }; +}; diff --git a/backend/src/services/membership-identity/membership-identity-types.ts b/backend/src/services/membership-identity/membership-identity-types.ts new file mode 100644 index 000000000..adce10237 --- /dev/null +++ b/backend/src/services/membership-identity/membership-identity-types.ts @@ -0,0 +1,82 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TMembershipIdentityScopeFactory { + onCreateMembershipIdentityGuard: (arg: TCreateMembershipIdentityDTO) => Promise; + + onUpdateMembershipIdentityGuard: (arg: TUpdateMembershipIdentityDTO) => Promise; + onDeleteMembershipIdentityGuard: (arg: TDeleteMembershipIdentityDTO) => Promise; + onListMembershipIdentityGuard: (arg: TListMembershipIdentityDTO) => Promise; + onGetMembershipIdentityByIdentityIdGuard: (arg: TGetMembershipIdentityByIdentityIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + getScopeDatabaseFields: (scope: AccessScopeData) => { + scopeOrgId: string; + scopeNamespaceId?: string | null; + scopeProjectId?: string | null; + }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + identityId: string; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TUpdateMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; + data: { + isActive?: boolean; + metadata?: { key: string; value: string }[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TListMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; + data: { + limit?: number; + offset?: number; + identityName?: string; + roles: string[]; + }; +}; + +export type TDeleteMembershipIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; +}; + +export type TGetMembershipIdentityByIdentityIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; +}; diff --git a/backend/src/services/membership-identity/namespace/namespace-membership-identity-factory.ts b/backend/src/services/membership-identity/namespace/namespace-membership-identity-factory.ts new file mode 100644 index 000000000..be3ca6ea9 --- /dev/null +++ b/backend/src/services/membership-identity/namespace/namespace-membership-identity-factory.ts @@ -0,0 +1,64 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; + +type TNamespaceMembershipIdentityScopeFactoryDep = Record; + +export const newNamespaceMembershipIdentityFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceMembershipIdentityScopeFactoryDep +): TMembershipIdentityScopeFactory => { + const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const getScopeDatabaseFields: TMembershipIdentityScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = () => { + throw new InternalServerError({ message: "Namespace membership identity isCustomRole not implemented" }); + }; + + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity create not implemented" }); + }; + + const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity update not implemented" }); + }; + + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity delete not implemented" }); + }; + + const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership identity list not implemented" }); + }; + + const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership identity get by identity id not implemented" }); + }; + + return { + onCreateMembershipIdentityGuard, + onUpdateMembershipIdentityGuard, + onDeleteMembershipIdentityGuard, + onListMembershipIdentityGuard, + onGetMembershipIdentityByIdentityIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts new file mode 100644 index 000000000..06789e274 --- /dev/null +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -0,0 +1,130 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { OrgPermissionIdentityActions, 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 { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; + +type TOrgMembershipIdentityScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; +}; + +export const newOrgMembershipIdentityFactory = ({ + permissionService, + orgDAL +}: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { + const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const getScopeDatabaseFields: TMembershipIdentityScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { scopeOrgId: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = + async () => { + throw new BadRequestError({ + message: "Organization membership cannot be created for organization scoped identity" + }); + }; + + const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + const permissionRoles = await permissionService.getOrgPermissionByRoles( + dto.data.roles.map((el) => el.role), + dto.permission.orgId + ); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity org membership", + shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GrantPrivileges, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = + async () => { + throw new BadRequestError({ + message: "Organization membership cannot be deleted for organization scoped identity" + }); + }; + + const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + }; + + const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = + async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + }; + + return { + onCreateMembershipIdentityGuard, + onUpdateMembershipIdentityGuard, + onDeleteMembershipIdentityGuard, + onListMembershipIdentityGuard, + onGetMembershipIdentityByIdentityIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-identity/project/project-membership-identity-factory.ts b/backend/src/services/membership-identity/project/project-membership-identity-factory.ts new file mode 100644 index 000000000..f2896047f --- /dev/null +++ b/backend/src/services/membership-identity/project/project-membership-identity-factory.ts @@ -0,0 +1,208 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionIdentityActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TMembershipIdentityDALFactory } from "../membership-identity-dal"; +import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; + +type TProjectMembershipIdentityScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipIdentityDAL: Pick; +}; + +export const newProjectMembershipIdentityFactory = ({ + permissionService, + orgDAL, + membershipIdentityDAL +}: TProjectMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { + const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const getScopeDatabaseFields: TMembershipIdentityScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role); + + const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + ProjectPermissionSub.Identity + ); + const orgMembership = await membershipIdentityDAL.findOne({ + actorIdentityId: dto.data.identityId, + scopeOrgId: dto.permission.orgId, + scope: AccessScope.Organization + }); + if (!orgMembership) + throw new BadRequestError({ message: `Identity ${dto.data.identityId} is missing organization membership` }); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const permissionRoles = await permissionService.getProjectPermissionByRoles( + dto.data.roles.map((el) => el.role), + scope.value + ); + for (const permissionRole of permissionRoles) { + if (permissionRole?.role?.name !== ProjectMembershipRole.NoAccess) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create identity project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + } + }; + + const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + ProjectPermissionSub.Identity + ); + + 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), + scope.value + ); + for (const permissionRole of permissionRoles) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update identity project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.GrantPrivileges, + ProjectPermissionSub.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + }; + + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Delete, + ProjectPermissionSub.Identity + ); + }; + + const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); + }; + + const onGetMembershipIdentityByIdentityIdGuard: TMembershipIdentityScopeFactory["onGetMembershipIdentityByIdentityIdGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); + }; + + return { + onCreateMembershipIdentityGuard, + onUpdateMembershipIdentityGuard, + onDeleteMembershipIdentityGuard, + onListMembershipIdentityGuard, + onGetMembershipIdentityByIdentityIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts new file mode 100644 index 000000000..7882b9639 --- /dev/null +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -0,0 +1,295 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, MembershipsSchema, TableName } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db"; +import { TSearchResourceOperator } from "@app/lib/search-resource/search"; + +export type TMembershipUserDALFactory = ReturnType; + +type TFindUserArg = { + scopeData: AccessScopeData; + tx?: Knex; + filter: Partial<{ + limit: number; + offset: number; + userId?: string; + username: Omit; + role: Omit; + }>; +}; + +type TGetUserByIdArg = { + scopeData: AccessScopeData; + tx?: Knex; + userId: string; +}; + +export const membershipUserDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + + const getUserById = async ({ scopeData, tx, userId }: TGetUserByIdArg) => { + try { + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Membership}.actorUserId`, userId) + .where(`${TableName.Users}.isGhost`, false) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("id").withSchema(TableName.Role).as("customRoleId"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("username").withSchema(TableName.Users).as("userUsername"), + db.ref("email").withSchema(TableName.Users).as("userEmail"), + db.ref("firstName").withSchema(TableName.Users).as("userFirstName"), + db.ref("lastName").withSchema(TableName.Users).as("userLastName"), + db.ref("id").withSchema(TableName.Users).as("userId") + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...MembershipsSchema.parse(el), + user: { + username: el.userUsername, + email: el.userEmail, + firstName: el.userFirstName, + lastName: el.userLastName, + id: el.userId + } + }), + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + customRoleSlug, + customRoleName, + customRoleId, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug, + customRoleName, + customRoleId, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + }, + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return data?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipGetByUserId" }); + } + }; + + const findUsers = async ({ scopeData, tx, filter }: TFindUserArg) => { + try { + const paginatedUsers = (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .where(`${TableName.Users}.isGhost`, false) + .where((qb) => { + if (scopeData.scope === AccessScope.Organization) { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + } else if (scopeData.scope === AccessScope.Namespace) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Namespace) + .where(`${TableName.Membership}.scopeNamespaceId`, scopeData.namespaceId); + } else if (scopeData.scope === AccessScope.Project) { + void qb + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId); + } + }); + + if (filter.limit) void paginatedUsers.limit(filter.limit); + if (filter.offset) void paginatedUsers.offset(filter.offset); + + if (filter.username || filter.role) { + buildKnexFilterForSearchResource( + paginatedUsers, + { + username: filter.username!, + role: filter.role! + }, + (attr) => { + switch (attr) { + case "role": + return [`${TableName.Role}.slug`, `${TableName.MembershipRole}.role`]; + case "username": + return `${TableName.Users}.username`; + default: + throw new BadRequestError({ message: `Invalid ${String(attr)} provided` }); + } + } + ); + } + + const docs = await (tx || db.replicaNode())(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) + .distinct(`${TableName.Membership}.id`) + .where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId) + .whereIn(`${TableName.Membership}.id`, paginatedUsers) + .select(selectAllTableCols(TableName.Membership)) + .select( + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("id").withSchema(TableName.Role).as("customRoleId"), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("role").withSchema(TableName.MembershipRole).as("membershipRole"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.MembershipRole).as("membershipRoleIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.MembershipRole).as("membershipRoleTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.MembershipRole) + .as("membershipRoleTemporaryAccessEndTime"), + db.ref("createdAt").withSchema(TableName.MembershipRole).as("membershipRoleCreatedAt"), + db.ref("updatedAt").withSchema(TableName.MembershipRole).as("membershipRoleUpdatedAt"), + db.ref("username").withSchema(TableName.Users).as("userUsername"), + db.ref("email").withSchema(TableName.Users).as("userEmail"), + db.ref("firstName").withSchema(TableName.Users).as("userFirstName"), + db.ref("lastName").withSchema(TableName.Users).as("userLastName"), + db.ref("id").withSchema(TableName.Users).as("userId") + ) + .select( + db.raw( + `count(${TableName.Membership}."actorUserId") OVER(PARTITION BY ${TableName.Membership}."scopeOrgId") as total` + ) + ); + + const data = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...MembershipsSchema.parse(el), + user: { + username: el.userUsername, + email: el.userEmail, + firstName: el.userFirstName, + lastName: el.userLastName, + id: el.userId + } + }), + childrenMapper: [ + { + key: "membershipRoleId", + label: "roles" as const, + mapper: ({ + customRoleSlug, + customRoleName, + customRoleId, + membershipRoleId, + membershipRole, + membershipRoleIsTemporary, + membershipRoleTemporaryMode, + membershipRoleTemporaryRange, + membershipRoleTemporaryAccessEndTime, + membershipRoleTemporaryAccessStartTime, + membershipRoleCreatedAt, + membershipRoleUpdatedAt + }) => ({ + id: membershipRoleId, + role: membershipRole, + customRoleSlug, + customRoleName, + customRoleId, + temporaryRange: membershipRoleTemporaryRange, + temporaryMode: membershipRoleTemporaryMode, + temporaryAccessStartTime: membershipRoleTemporaryAccessStartTime, + temporaryAccessEndTime: membershipRoleTemporaryAccessEndTime, + isTemporary: membershipRoleIsTemporary, + createdAt: membershipRoleCreatedAt, + updatedAt: membershipRoleUpdatedAt + }) + } + ] + }); + return { data, totalCount: Number((data?.[0] as unknown as { total: number })?.total ?? 0) }; + } catch (error) { + throw new DatabaseError({ error, name: "MembershipfindUser" }); + } + }; + + return { ...orm, findUsers, getUserById }; +}; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts new file mode 100644 index 000000000..82dca0159 --- /dev/null +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -0,0 +1,481 @@ +import { + AccessScope, + OrgMembershipStatus, + 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"; +import { groupBy } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; +import { SearchResourceOperators } from "@app/lib/search-resource/search"; + +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +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, + TDeleteMembershipUserDTO, + TGetMembershipUserByUserIdDTO, + TListMembershipUserDTO, + TUpdateMembershipUserDTO +} from "./membership-user-types"; +import { newNamespaceMembershipUserFactory } from "./namespace/namespace-membership-user-factory"; +import { newOrgMembershipUserFactory } from "./org/org-membership-user-factory"; +import { newProjectMembershipUserFactory } from "./project/project-membership-user-factory"; + +type TMembershipUserServiceFactoryDep = { + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: Pick; + orgDAL: Pick; + roleDAL: Pick; + userDAL: TUserDALFactory; + permissionService: Pick< + TPermissionServiceFactory, + "getProjectPermission" | "getProjectPermissionByRoles" | "getOrgPermission" + >; + licenseService: TLicenseServiceFactory; + projectKeyDAL: TProjectKeyDALFactory; + userAliasDAL: TUserAliasDALFactory; + smtpService: TSmtpService; + tokenService: TAuthTokenServiceFactory; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + projectDAL: TProjectDALFactory; + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; +}; + +export type TMembershipUserServiceFactory = ReturnType; + +export const membershipUserServiceFactory = ({ + membershipUserDAL, + roleDAL, + membershipRoleDAL, + userDAL, + permissionService, + orgDAL, + projectKeyDAL, + userAliasDAL, + licenseService, + smtpService, + tokenService, + userGroupMembershipDAL, + projectDAL, + additionalPrivilegeDAL +}: TMembershipUserServiceFactoryDep) => { + const scopeFactory = { + [AccessScope.Organization]: newOrgMembershipUserFactory({ + permissionService, + licenseService, + smtpService, + orgDAL, + tokenService, + userDAL, + userGroupMembershipDAL + }), + [AccessScope.Namespace]: newNamespaceMembershipUserFactory({}), + [AccessScope.Project]: newProjectMembershipUserFactory({ + orgDAL, + permissionService, + membershipUserDAL, + projectDAL, + smtpService + }) + }; + + const $getUsers = async (usernames: string[]) => { + const existingUsers = await userDAL.find({ $in: { username: usernames } }); + if (existingUsers.length !== usernames.length) { + const newUserEmails = usernames.filter( + (inviteeEmail) => !existingUsers.find((el) => el.username === inviteeEmail) + ); + await userDAL.transaction(async (tx) => { + for await (const inviteeEmail of newUserEmails) { + const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx); + let inviteeUser = + usersByUsername?.length > 1 + ? usersByUsername.find((el) => el.username === inviteeEmail) + : usersByUsername?.[0]; + + // if the user doesn't exist we create the user with the email + if (!inviteeUser) { + // TODO(carlos): will be removed once the function receives usernames instead of emails + const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx); + if (usersByEmail?.length === 1) { + [inviteeUser] = usersByEmail; + } else { + inviteeUser = await userDAL.create( + { + isAccepted: false, + email: inviteeEmail, + username: inviteeEmail, + authMethods: [AuthMethod.EMAIL], + isGhost: false + }, + tx + ); + } + } + + existingUsers.push(inviteeUser); + const inviteeUserId = inviteeUser?.id; + const existingEncryptionKey = 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 && !existingEncryptionKey)) { + await userDAL.createUserEncryption( + { + userId: inviteeUserId, + encryptionVersion: 2 + }, + tx + ); + } + } + }); + } + return existingUsers; + }; + + const createMembership = async (dto: TCreateMembershipUserDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "User must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const users = await $getUsers(dto.data.usernames); + const existingMemberships = await membershipUserDAL.find({ + scope: scopeData.scope, + ...scopeDatabaseFields, + $in: { + actorUserId: users.map((el) => el.id) + } + }); + + if (existingMemberships.length === users.length) return { memberships: [] }; + + 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, + actorUserId: user.id, + status: scopeData.scope === AccessScope.Organization ? OrgMembershipStatus.Invited : undefined, + inviteEmail: scopeData.scope === AccessScope.Organization ? user.email : undefined + })); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + if (hasCustomRole) { + const plan = await licenseService.getPlan(scopeData.orgId); + if (!plan?.rbac) + throw new BadRequestError({ + message: + "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." + }); + } + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipUserDAL.transaction(async (tx) => { + const docs = await membershipUserDAL.insertMany(newMemberships, tx); + + const roleDocs: TMembershipRolesInsert[] = []; + docs.forEach((membership) => { + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: membership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: membership.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + }); + await membershipRoleDAL.insertMany(roleDocs, tx); + return docs; + }); + + const { signUpTokens } = await factory.onCreateMembershipComplete(dto, newMembershipUsers); + return { memberships: membershipDoc, signUpTokens }; + }; + + const updateMembership = async (dto: TUpdateMembershipUserDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateMembershipUserGuard(dto); + + const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); + const hasCustomRole = customInputRoles.length > 0; + if (hasCustomRole) { + const plan = await licenseService.getPlan(scopeData.orgId); + if (!plan?.rbac) + throw new BadRequestError({ + message: + "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." + }); + } + + const hasNoPermanentRole = data.roles.every((el) => el.isTemporary); + if (hasNoPermanentRole) { + throw new BadRequestError({ + message: "User must have at least one permanent role" + }); + } + const isInvalidTemporaryRole = data.roles.some((el) => { + if (el.isTemporary) { + if (!el.temporaryAccessStartTime || !el.temporaryRange) { + return true; + } + } + return false; + }); + if (isInvalidTemporaryRole) { + throw new BadRequestError({ + message: "Temporary role must have access start time and range" + }); + } + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipUserDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorUserId: dto.selector.userId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "User doesn't have membership" + }); + + const scopeField = factory.getScopeField(dto.scopeData); + const customRoles = hasCustomRole + ? await roleDAL.find({ + [scopeField.key]: scopeField.value, + $in: { slug: customInputRoles.map(({ role }) => role) } + }) + : []; + if (customRoles.length !== customInputRoles.length) { + throw new NotFoundError({ message: "One or more custom roles not found" }); + } + + const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); + + const membershipDoc = await membershipUserDAL.transaction(async (tx) => { + const doc = + typeof data?.isActive === "undefined" + ? existingMembership + : await membershipUserDAL.updateById( + existingMembership.id, + { + isActive: data.isActive + }, + tx + ); + + const roleDocs: TMembershipRolesInsert[] = []; + data.roles.forEach((membershipRole) => { + const isCustomRole = Boolean(customRolesGroupBySlug?.[membershipRole.role]?.[0]); + if (membershipRole.isTemporary) { + const relativeTimeInMs = membershipRole.temporaryRange ? ms(membershipRole.temporaryRange) : null; + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null, + isTemporary: true, + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: membershipRole.temporaryRange, + temporaryAccessStartTime: new Date(membershipRole.temporaryAccessStartTime as string), + temporaryAccessEndTime: new Date( + new Date(membershipRole.temporaryAccessStartTime as string).getTime() + (relativeTimeInMs as number) + ) + }); + } else { + roleDocs.push({ + membershipId: doc.id, + role: isCustomRole ? ProjectMembershipRole.Custom : membershipRole.role, + customRoleId: customRolesGroupBySlug[membershipRole.role] + ? customRolesGroupBySlug[membershipRole.role][0].id + : null + }); + } + }); + await membershipRoleDAL.delete( + { + membershipId: doc.id + }, + tx + ); + const insertedRoles = await membershipRoleDAL.insertMany(roleDocs, tx); + return { ...doc, roles: insertedRoles }; + }); + + return { membership: membershipDoc }; + }; + + const deleteMembership = async (dto: TDeleteMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteMembershipUserGuard(dto); + + const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData); + const existingMembership = await membershipUserDAL.findOne({ + scope: scopeData.scope, + ...scopeDatabaseFields, + actorUserId: dto.selector.userId + }); + if (!existingMembership) + throw new BadRequestError({ + message: "User doesn't have membership" + }); + + if (existingMembership.actorUserId === dto.permission.id) + throw new BadRequestError({ + message: "You can't delete your 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, + additionalPrivilegeDAL + }); + return doc; + } + + if (dto.scopeData.scope === AccessScope.Project) { + await additionalPrivilegeDAL.delete( + { + actorUserId: dto.selector.userId, + projectId: dto.scopeData.projectId + }, + tx + ); + } + + await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); + const doc = await membershipUserDAL.deleteById(existingMembership.id, tx); + return doc; + }); + return { membership: membershipDoc }; + }; + + const listMemberships = async (dto: TListMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipUserGuard(dto); + const memberships = await membershipUserDAL.findUsers({ + scopeData, + filter: { + limit: dto.data.limit, + offset: dto.data.offset, + username: dto.data.username, + role: dto.data?.roles?.length + ? { + [SearchResourceOperators.$in]: dto.data.roles + } + : undefined + } + }); + return memberships; + }; + + const getMembershipByUserId = async (dto: TGetMembershipUserByUserIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetMembershipUserByUserIdGuard(dto); + const membership = await membershipUserDAL.getUserById({ + scopeData, + userId: selector.userId + }); + if (!membership) throw new NotFoundError({ message: `User membership not found` }); + + return membership; + }; + + return { + createMembership, + updateMembership, + deleteMembership, + listMemberships, + getMembershipByUserId + }; +}; diff --git a/backend/src/services/membership-user/membership-user-types.ts b/backend/src/services/membership-user/membership-user-types.ts new file mode 100644 index 000000000..b8761671c --- /dev/null +++ b/backend/src/services/membership-user/membership-user-types.ts @@ -0,0 +1,95 @@ +import { AccessScopeData, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TMembershipUserScopeFactory { + 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 | TBulkDeleteMembershipByUsernameDTO) => Promise; + + onListMembershipUserGuard: (arg: TListMembershipUserDTO) => Promise; + onGetMembershipUserByUserIdGuard: (arg: TGetMembershipUserByUserIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + getScopeDatabaseFields: (scope: AccessScopeData) => { + scopeOrgId: string; + scopeNamespaceId?: string | null; + scopeProjectId?: string | null; + }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + usernames: string[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TUpdateMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + userId: string; + }; + data: { + isActive?: boolean; + metadata?: { key: string; value: string }[]; + roles: { + role: string; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }[]; + }; +}; + +export type TListMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + limit?: number; + offset?: number; + username?: string; + roles?: string[]; + }; +}; + +export type TDeleteMembershipUserDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + userId: string; + }; +}; + +export type TBulkDeleteMembershipByUsernameDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + usernames: string[]; + }; +}; + +export type TGetMembershipUserByUserIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + userId: string; + }; +}; diff --git a/backend/src/services/membership-user/namespace/namespace-membership-user-factory.ts b/backend/src/services/membership-user/namespace/namespace-membership-user-factory.ts new file mode 100644 index 000000000..fb2cfc463 --- /dev/null +++ b/backend/src/services/membership-user/namespace/namespace-membership-user-factory.ts @@ -0,0 +1,66 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TMembershipUserScopeFactory } from "../membership-user-types"; + +type TNamespaceMembershipUserScopeFactoryDep = Record; + +export const newNamespaceMembershipUserFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceMembershipUserScopeFactoryDep +): TMembershipUserScopeFactory => { + const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { scopeOrgId: dto.orgId, scopeNamespaceId: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = () => { + throw new InternalServerError({ message: "Namespace membership user isCustomRole not implemented" }); + }; + + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user create not implemented" }); + }; + + const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async () => { + throw new InternalServerError({ message: "Namespace membership user create complete not implemented" }); + }; + + const onUpdateMembershipUserGuard: TMembershipUserScopeFactory["onUpdateMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user update not implemented" }); + }; + + const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user delete not implemented" }); + }; + + const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async () => { + throw new InternalServerError({ message: "Namespace membership user list not implemented" }); + }; + + const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace membership user get by user id not implemented" }); + }; + + return { + onCreateMembershipUserGuard, + onCreateMembershipComplete, + onUpdateMembershipUserGuard, + onDeleteMembershipUserGuard, + onListMembershipUserGuard, + onGetMembershipUserByUserIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; 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 new file mode 100644 index 000000000..523e85bae --- /dev/null +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -0,0 +1,193 @@ +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 { 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, + tokenService, + userDAL, + orgDAL, + smtpService, + licenseService +}: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { + const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { scopeOrgId: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); + + const 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 ( + 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) { + if (!appCfg.isSmtpConfigured) { + 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) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); + }; + + const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); + }; + + const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + }; + + const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); + }; + + return { + onCreateMembershipUserGuard, + onCreateMembershipComplete, + onUpdateMembershipUserGuard, + onDeleteMembershipUserGuard, + onListMembershipUserGuard, + onGetMembershipUserByUserIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; 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 new file mode 100644 index 000000000..0a9bb955a --- /dev/null +++ b/backend/src/services/membership-user/project/project-membership-user-factory.ts @@ -0,0 +1,239 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionMemberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +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, + projectDAL, + membershipUserDAL, + smtpService +}: TProjectMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { + const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const getScopeDatabaseFields: TMembershipUserScopeFactory["getScopeDatabaseFields"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { scopeOrgId: dto.orgId, scopeProjectId: dto.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role) => isCustomProjectRole(role); + + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async ( + dto, + newUsers + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); + + // 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), + scope.value + ); + + for (const permissionRole of permissionRoles) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to create user project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + }; + + const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( + dto, + newMembers + ) => { + const orgMembershipAccepted = await membershipUserDAL.find({ + scope: AccessScope.Organization, + scopeOrgId: dto.permission.orgId, + status: OrgMembershipStatus.Accepted, + $in: { + actorUserId: newMembers.map((el) => el.id) + } + }); + + if (!orgMembershipAccepted.length) return { signUpTokens: [] }; + + const appCfg = getConfig(); + const scope = getScopeField(dto.scopeData); + const project = await projectDAL.findById(scope.value); + + const orgMembershipAcceptedUserIds = orgMembershipAccepted.map((el) => el.actorUserId as string); + const emails = newMembers + .filter((el) => Boolean(el?.email) && orgMembershipAcceptedUserIds.includes(el.id)) + .map((el) => el?.email as string); + if (emails.length) { + 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) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + + const { 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), + scope.value + ); + + for (const permissionRole of permissionRoles) { + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member, + permission, + permissionRole.permission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update user project membership", + shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.GrantPrivileges, + ProjectPermissionSub.Member + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + } + }; + + const onDeleteMembershipUserGuard: TMembershipUserScopeFactory["onDeleteMembershipUserGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); + }; + + const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + }; + + const onGetMembershipUserByUserIdGuard: TMembershipUserScopeFactory["onGetMembershipUserByUserIdGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + }; + + return { + onCreateMembershipUserGuard, + onCreateMembershipComplete, + onUpdateMembershipUserGuard, + onDeleteMembershipUserGuard, + onListMembershipUserGuard, + onGetMembershipUserByUserIdGuard, + getScopeField, + getScopeDatabaseFields, + isCustomRole + }; +}; diff --git a/backend/src/services/membership/membership-dal.ts b/backend/src/services/membership/membership-dal.ts new file mode 100644 index 000000000..03ee6af92 --- /dev/null +++ b/backend/src/services/membership/membership-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TMembershipDALFactory = ReturnType; + +export const membershipDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + return orm; +}; diff --git a/backend/src/services/membership/membership-role-dal.ts b/backend/src/services/membership/membership-role-dal.ts new file mode 100644 index 000000000..bb7011221 --- /dev/null +++ b/backend/src/services/membership/membership-role-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TMembershipRoleDALFactory = ReturnType; + +export const membershipRoleDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.MembershipRole); + return orm; +}; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 995afadc1..4c080717d 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,26 +1,25 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; +import { AccessScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, 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 { 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 { TProjectDALFactory } from "../project/project-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; type TOrgAdminServiceFactoryDep = { permissionService: Pick; projectDAL: Pick; - projectMembershipDAL: Pick< - TProjectMembershipDALFactory, - "findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers" - >; - projectUserMembershipRoleDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; smtpService: Pick; notificationService: Pick; }; @@ -31,9 +30,10 @@ export const orgAdminServiceFactory = ({ permissionService, projectDAL, projectMembershipDAL, - projectUserMembershipRoleDAL, smtpService, - notificationService + notificationService, + membershipUserDAL, + membershipRoleDAL }: TOrgAdminServiceFactoryDep) => { const listOrgProjects = async ({ actor, @@ -98,17 +98,18 @@ export const orgAdminServiceFactory = ({ } // check already there exist a membership if there return it - const projectMembership = await projectMembershipDAL.findOne({ - projectId, - userId: actorId + const projectMembership = await membershipUserDAL.findOne({ + scopeProjectId: projectId, + scope: AccessScope.Project, + actorUserId: actorId }); if (projectMembership) { // reset and make the user admin - await projectMembershipDAL.transaction(async (tx) => { - await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx); - await projectUserMembershipRoleDAL.create( + await membershipUserDAL.transaction(async (tx) => { + await membershipRoleDAL.delete({ membershipId: projectMembership.id }, tx); + await membershipRoleDAL.create( { - projectMembershipId: projectMembership.id, + membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, tx @@ -117,18 +118,17 @@ export const orgAdminServiceFactory = ({ return { isExistingMember: true, membership: projectMembership }; } - const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { - const newProjectMembership = await projectMembershipDAL.create( + const updatedMembership = await membershipUserDAL.transaction(async (tx) => { + const newProjectMembership = await membershipUserDAL.create( { - projectId, - userId: actorId + scopeProjectId: projectId, + actorUserId: actorId, + scope: AccessScope.Project, + scopeOrgId: actorOrgId }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); + await membershipRoleDAL.create({ membershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin }, tx); return newProjectMembership; }); diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts index 2d7992d34..c52c2834a 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -1,19 +1,21 @@ import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, sqlNestRelationships } from "@app/lib/knex"; +import { sqlNestRelationships } from "@app/lib/knex"; export type TOrgMembershipDALFactory = ReturnType; export const orgMembershipDALFactory = (db: TDbClient) => { - const orgMembershipOrm = ormify(db, TableName.OrgMembership); - const findOrgMembershipById = async (membershipId: string) => { try { const member = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.id`, membershipId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.id`, membershipId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.Role}.id`, `${TableName.MembershipRole}.customRoleId`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, @@ -21,19 +23,20 @@ export const orgMembershipDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("inviteEmail").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("isActive").withSchema(TableName.OrgMembership), - db.ref("lastLoginAuthMethod").withSchema(TableName.OrgMembership), - db.ref("lastLoginTime").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("inviteEmail").withSchema(TableName.Membership), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("status").withSchema(TableName.Membership), + db.ref("isActive").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -55,6 +58,7 @@ export const orgMembershipDALFactory = (db: TDbClient) => { parentMapper: ({ email, isEmailVerified, + customRoleSlug, username, firstName, lastName, @@ -74,6 +78,7 @@ export const orgMembershipDALFactory = (db: TDbClient) => { orgId, id, role, + customRoleSlug, status, isActive, inviteEmail, @@ -117,18 +122,20 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const twelveMonthsAgo = new Date(now.getTime() - 360 * 24 * 60 * 60 * 1000); const memberships = await db - .replicaNode()(TableName.OrgMembership) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) .where("status", "invited") .where((qb) => { // lastInvitedAt is null AND createdAt is between 1 week and 12 months ago void qb - .whereNull(`${TableName.OrgMembership}.lastInvitedAt`) - .whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]); + .whereNull(`${TableName.Membership}.lastInvitedAt`) + .whereBetween(`${TableName.Membership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]); // lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago void qb.orWhere((qbInner) => { void qbInner - .where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo) - .where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo); + .where(`${TableName.Membership}.lastInvitedAt`, "<", oneWeekAgo) + .where(`${TableName.Membership}.createdAt`, ">", oneMonthAgo); }); }); @@ -144,7 +151,11 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const updateLastInvitedAtByIds = async (membershipIds: string[]) => { try { if (membershipIds.length === 0) return; - await db(TableName.OrgMembership).whereIn("id", membershipIds).update({ lastInvitedAt: new Date() }); + await db(TableName.Membership) + .whereIn("id", membershipIds) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .update({ lastInvitedAt: new Date() }); } catch (error) { throw new DatabaseError({ error, @@ -156,9 +167,12 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const findOrgMembershipsWithUsersByOrgId = async (orgId: string) => { try { const members = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, @@ -166,17 +180,17 @@ export const orgMembershipDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder - .on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`) - .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`); + .on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`); }) .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("inviteEmail").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("isActive").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("inviteEmail").withSchema(TableName.Membership), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("customRoleId"), + db.ref("status").withSchema(TableName.Membership), + db.ref("isActive").withSchema(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -207,7 +221,6 @@ export const orgMembershipDALFactory = (db: TDbClient) => { }; return { - ...orgMembershipOrm, findOrgMembershipById, findRecentInvitedMemberships, updateLastInvitedAtByIds, diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 6ac363c76..288926f90 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -2,14 +2,15 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, OrganizationsSchema, OrgMembershipRole, TableName, + TMemberships, + TMembershipsInsert, + TMembershipsUpdate, TOrganizations, TOrganizationsInsert, - TOrgMemberships, - TOrgMembershipsInsert, - TOrgMembershipsUpdate, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; @@ -64,9 +65,12 @@ export const orgDALFactory = (db: TDbClient) => { .replicaNode()(TableName.Organization) .whereIn(`${TableName.Organization}.id`, orgIdSubquery) .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) - .leftJoin(TableName.OrgMembership, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) - .leftJoin(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`) + .leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .leftJoin(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .leftJoin(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .where((qb) => { void qb.where(`${TableName.Users}.isGhost`, false).orWhereNull(`${TableName.Users}.id`); }); @@ -84,11 +88,11 @@ export const orgDALFactory = (db: TDbClient) => { .select(db.ref("firstName").withSchema(TableName.Users).as("firstName")) .select(db.ref("lastName").withSchema(TableName.Users).as("lastName")) .select(db.ref("id").withSchema(TableName.Users).as("userId")) - .select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId")) - .select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole")) - .select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId")) - .select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus")) - .select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName")), + .select(db.ref("id").withSchema(TableName.Membership).as("orgMembershipId")) + .select(db.ref("status").withSchema(TableName.Membership).as("orgMembershipStatus")) + .select(db.ref("role").withSchema(TableName.MembershipRole).as("orgMembershipRole")) + .select(db.ref("customRoleId").withSchema(TableName.MembershipRole).as("orgMembershipRoleId")) + .select(db.ref("name").withSchema(TableName.Role).as("orgMembershipRoleName")), buildBaseQuery(countQuery).countDistinct(`${TableName.Organization}.id`, { as: "count" }).first() ]); @@ -229,9 +233,12 @@ export const orgDALFactory = (db: TDbClient) => { ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string; userStatus: string })[]> => { try { const org = (await db - .replicaNode()(TableName.OrgMembership) - .where({ userId }) - .join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.actorUserId`, userId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -247,8 +254,8 @@ export const orgDALFactory = (db: TDbClient) => { ); }) .select(selectAllTableCols(TableName.Organization)) - .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) - .select(db.ref("status").withSchema(TableName.OrgMembership).as("userStatus")) + .select(db.ref("role").withSchema(TableName.MembershipRole).as("userRole")) + .select(db.ref("status").withSchema(TableName.Membership).as("userStatus")) .select( db.raw(` CASE @@ -283,24 +290,28 @@ export const orgDALFactory = (db: TDbClient) => { const findAllOrgMembers = async (orgId: string) => { try { const members = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("inviteEmail").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("isActive").withSchema(TableName.OrgMembership), - db.ref("lastLoginAuthMethod").withSchema(TableName.OrgMembership), - db.ref("lastLoginTime").withSchema(TableName.OrgMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("inviteEmail").withSchema(TableName.Membership), + db.ref("scopeOrgId").withSchema(TableName.Membership).as("orgId"), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), + db.ref("status").withSchema(TableName.Membership), + db.ref("isActive").withSchema(TableName.Membership), + db.ref("lastLoginAuthMethod").withSchema(TableName.Membership), + db.ref("lastLoginTime").withSchema(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), @@ -332,11 +343,13 @@ export const orgDALFactory = (db: TDbClient) => { } const count = await db - .replicaNode()(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) .count("*") - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .where({ isGhost: false, [`${TableName.OrgMembership}.isActive` as "isActive"]: true }) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .where({ isGhost: false, [`${TableName.Membership}.isActive` as "isActive"]: true }) .first(); return parseInt((count as unknown as CountResult).count || "0", 10); @@ -348,21 +361,25 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => { try { const conn = tx || db.replicaNode(); - const members = await conn(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + const members = await conn(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - conn.ref("id").withSchema(TableName.OrgMembership), - conn.ref("inviteEmail").withSchema(TableName.OrgMembership), - conn.ref("orgId").withSchema(TableName.OrgMembership), - conn.ref("role").withSchema(TableName.OrgMembership), - conn.ref("roleId").withSchema(TableName.OrgMembership), - conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("id").withSchema(TableName.Membership), + conn.ref("inviteEmail").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), conn.ref("username").withSchema(TableName.Users), conn.ref("email").withSchema(TableName.Users), conn.ref("firstName").withSchema(TableName.Users), @@ -384,22 +401,25 @@ export const orgDALFactory = (db: TDbClient) => { const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => { try { const conn = tx || db.replicaNode(); - const members = await conn(TableName.OrgMembership) - .where(`${TableName.OrgMembership}.orgId`, orgId) - .where(`${TableName.OrgMembership}.role`, role) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + const members = await conn(TableName.Membership) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .where(`${TableName.MembershipRole}.role`, role) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .leftJoin( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - conn.ref("id").withSchema(TableName.OrgMembership), - conn.ref("inviteEmail").withSchema(TableName.OrgMembership), - conn.ref("orgId").withSchema(TableName.OrgMembership), - conn.ref("role").withSchema(TableName.OrgMembership), - conn.ref("roleId").withSchema(TableName.OrgMembership), - conn.ref("status").withSchema(TableName.OrgMembership), + conn.ref("id").withSchema(TableName.Membership), + conn.ref("inviteEmail").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), conn.ref("username").withSchema(TableName.Users), conn.ref("email").withSchema(TableName.Users), conn.ref("firstName").withSchema(TableName.Users), @@ -418,47 +438,6 @@ export const orgDALFactory = (db: TDbClient) => { } }; - const findOrgGhostUser = async (orgId: string) => { - try { - const member = await db - .replicaNode()(TableName.OrgMembership) - .where({ orgId }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) - .select( - db.ref("id").withSchema(TableName.OrgMembership), - db.ref("orgId").withSchema(TableName.OrgMembership), - db.ref("role").withSchema(TableName.OrgMembership), - db.ref("roleId").withSchema(TableName.OrgMembership), - db.ref("status").withSchema(TableName.OrgMembership), - db.ref("email").withSchema(TableName.Users), - db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("publicKey").withSchema(TableName.UserEncryptionKey) - ) - .where({ isGhost: true }) - .first(); - return member; - } catch (error) { - return null; - } - }; - - const ghostUserExists = async (orgId: string) => { - try { - const member = await db - .replicaNode()(TableName.OrgMembership) - .where({ orgId }) - .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id`) - .select(db.ref("id").withSchema(TableName.Users).as("userId")) - .where({ isGhost: true }) - .first(); - return Boolean(member); - } catch (error) { - return false; - } - }; - const create = async (dto: TOrganizationsInsert, tx?: Knex) => { try { const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*"); @@ -491,38 +470,37 @@ export const orgDALFactory = (db: TDbClient) => { // MEMBERSHIP OPERATIONS // -------------------- - // const orgMembershipOrm = ormify(db, TableName.OrgMembership); - const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => { + const createMembership = async (data: TMembershipsInsert, tx?: Knex) => { try { - const [membership] = await (tx || db)(TableName.OrgMembership).insert(data).returning("*"); + const [membership] = await (tx || db)(TableName.Membership).insert(data).returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Create org membership" }); } }; - const bulkCreateMemberships = async (data: TOrgMembershipsInsert[], tx?: Knex) => { + const bulkCreateMemberships = async (data: TMembershipsInsert[], tx?: Knex) => { try { - const memberships = await (tx || db)(TableName.OrgMembership).insert(data).returning("*"); + const memberships = await (tx || db)(TableName.Membership).insert(data).returning("*"); return memberships; } catch (error) { throw new DatabaseError({ error, name: "Create org memberships" }); } }; - const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => { + const updateMembershipById = async (id: string, data: TMembershipsUpdate, tx?: Knex) => { try { - const [membership] = await (tx || db)(TableName.OrgMembership).where({ id }).update(data).returning("*"); + const [membership] = await (tx || db)(TableName.Membership).where({ id }).update(data).returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Update org membership" }); } }; - const updateMembership = async (filter: Partial, data: TOrgMembershipsUpdate, tx?: Knex) => { + const updateMembership = async (filter: Partial, data: TMembershipsUpdate, tx?: Knex) => { try { - const membership = await (tx || db)(TableName.OrgMembership).where(filter).update(data).returning("*"); + const membership = await (tx || db)(TableName.Membership).where(filter).update(data).returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Update org memberships" }); @@ -531,7 +509,10 @@ export const orgDALFactory = (db: TDbClient) => { const deleteMembershipById = async (id: string, orgId: string, tx?: Knex) => { try { - const [membership] = await (tx || db)(TableName.OrgMembership).where({ id, orgId }).delete().returning("*"); + const [membership] = await (tx || db)(TableName.Membership) + .where({ id, scopeOrgId: orgId, scope: AccessScope.Organization }) + .delete() + .returning("*"); return membership; } catch (error) { throw new DatabaseError({ error, name: "Delete org membership" }); @@ -540,9 +521,10 @@ export const orgDALFactory = (db: TDbClient) => { const deleteMembershipsById = async (ids: string[], orgId: string, tx?: Knex) => { try { - const memberships = await (tx || db)(TableName.OrgMembership) + const memberships = await (tx || db)(TableName.Membership) .where({ - orgId + scopeOrgId: orgId, + scope: AccessScope.Organization }) .whereIn("id", ids) .delete() @@ -554,22 +536,23 @@ export const orgDALFactory = (db: TDbClient) => { }; const findMembership = async ( - filter: TFindFilter, - { offset, limit, sort, tx }: TFindOpt = {} + filter: TFindFilter, + { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db.replicaNode())(TableName.OrgMembership) + const query = (tx || db.replicaNode())(TableName.Membership) // eslint-disable-next-line .where(buildFindFilter(filter)) - .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .where("scope", AccessScope.Organization) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { - this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`) - .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`) + this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) + .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) .andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"])); }) .select( - selectAllTableCols(TableName.OrgMembership), + selectAllTableCols(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), @@ -595,18 +578,20 @@ export const orgDALFactory = (db: TDbClient) => { const findMembershipWithScimFilter = async ( orgId: string, scimFilter: string | undefined, - { offset, limit, sort, tx }: TFindOpt = {} + { offset, limit, sort, tx }: TFindOpt = {} ) => { try { - const query = (tx || db.replicaNode())(TableName.OrgMembership) + const query = (tx || db.replicaNode())(TableName.Membership) // eslint-disable-next-line - .where(`${TableName.OrgMembership}.orgId`, orgId) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) .where((qb) => { if (scimFilter) { void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => { switch (attrPath) { case "active": - return `${TableName.OrgMembership}.isActive`; + return `${TableName.Membership}.isActive`; case "userName": return `${TableName.UserAliases}.externalId`; case "name.givenName": @@ -621,15 +606,15 @@ export const orgDALFactory = (db: TDbClient) => { }); } }) - .join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`) - .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { - this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`) - .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`) + this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) + .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) .andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"])); }) .select( - selectAllTableCols(TableName.OrgMembership), + selectAllTableCols(TableName.Membership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), @@ -658,13 +643,16 @@ export const orgDALFactory = (db: TDbClient) => { ): Promise<{ id: string; name: string; slug: string; role: string }> => { try { const org = await db - .replicaNode()(TableName.IdentityOrgMembership) - .where({ identityId }) - .join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`) + .replicaNode()(TableName.Membership) + .where({ actorIdentityId: identityId }) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) + .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) .select(db.ref("id").withSchema(TableName.Organization).as("id")) .select(db.ref("name").withSchema(TableName.Organization).as("name")) .select(db.ref("slug").withSchema(TableName.Organization).as("slug")) - .select(db.ref("role").withSchema(TableName.IdentityOrgMembership).as("role")); + .select(db.ref("role").withSchema(TableName.MembershipRole).as("role")); return org?.[0]; } catch (error) { @@ -681,10 +669,8 @@ export const orgDALFactory = (db: TDbClient) => { findOrgBySlug, findAllOrgsByUserId, findOrganizationsByFilter, - ghostUserExists, findOrgMembersByUsername, findOrgMembersByRole, - findOrgGhostUser, create, updateById, deleteById, diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 30e67cca4..78d52e816 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -1,129 +1,66 @@ +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 { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; 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"; -type TDeleteOrgMembership = { - orgMembershipId: string; - orgId: string; - orgDAL: Pick; - projectMembershipDAL: Pick; - projectKeyDAL: Pick; - userAliasDAL: Pick; - licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; - userId?: string; -}; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; - orgDAL: Pick; - projectMembershipDAL: Pick; + orgDAL: Pick; + userGroupMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipRoleDAL: Pick; projectKeyDAL: Pick; userAliasDAL: Pick; licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; userId?: string; -}; - -export const deleteOrgMembershipFn = async ({ - orgMembershipId, - orgId, - orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, - projectKeyDAL, - userAliasDAL, - licenseService, - userId -}: TDeleteOrgMembership) => { - const deletedMembership = await orgDAL.transaction(async (tx) => { - const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); - - if (userId && orgMembership.userId === 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" }); - } - - if (!orgMembership.userId) { - await licenseService.updateSubscriptionOrgMemberCount(orgId); - return orgMembership; - } - - await userAliasDAL.delete( - { - userId: orgMembership.userId, - orgId - }, - tx - ); - - await projectUserAdditionalPrivilegeDAL.delete( - { - userId: orgMembership.userId - }, - tx - ); - - // Get all the project memberships of the user in the organization - const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId); - - // Delete all the project memberships of the user in the organization - await projectMembershipDAL.delete( - { - $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: orgMembership.userId - }); - - // 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; + additionalPrivilegeDAL: Pick; }; export const deleteOrgMembershipsFn = async ({ orgMembershipIds, orgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL, + userGroupMembershipDAL, + membershipRoleDAL, + additionalPrivilegeDAL }: 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.userId)) - .map((member) => member.userId) as string[]; + .filter((member) => Boolean(member.actorUserId)) + .map((member) => member.actorUserId) as string[]; if (userId && membershipUserIds.includes(userId)) { // scott: this is temporary, we will add a leave org endpoint with proper handling to ensure org isn't abandoned/broken @@ -145,41 +82,55 @@ export const deleteOrgMembershipsFn = async ({ tx ); - await projectUserAdditionalPrivilegeDAL.delete( - { - $in: { - userId: membershipUserIds - } - }, - tx - ); - // 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 projectMembershipDAL.delete( + const otherMemberships = await membershipUserDAL.delete( { + 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); + + await additionalPrivilegeDAL.delete( + { + $in: { + projectId: projectIds, + actorUserId: membershipUserIds + } + }, + tx + ); + // 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-dal.ts b/backend/src/services/org/org-role-dal.ts deleted file mode 100644 index 2bc57001a..000000000 --- a/backend/src/services/org/org-role-dal.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TOrgRoleDALFactory = ReturnType; - -export const orgRoleDALFactory = (db: TDbClient) => ormify(db, TableName.OrgRoles); diff --git a/backend/src/services/org/org-role-fns.ts b/backend/src/services/org/org-role-fns.ts index 5bff1e324..96e01b405 100644 --- a/backend/src/services/org/org-role-fns.ts +++ b/backend/src/services/org/org-role-fns.ts @@ -1,7 +1,8 @@ import { OrgMembershipRole } from "@app/db/schemas"; import { TFeatureSet } from "@app/ee/services/license/license-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; -import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; + +import { TRoleDALFactory } from "../role/role-dal"; const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom"); @@ -10,13 +11,13 @@ export const isCustomOrgRole = (roleSlug: string) => !RESERVED_ORG_ROLE_SLUGS.fi // this is only for updating an org export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ membershipRoleSlug, - orgRoleDAL, + roleDAL, plan, orgId }: { orgId: string; membershipRoleSlug: string; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; plan: TFeatureSet; }) => { if (isCustomOrgRole(membershipRoleSlug)) { @@ -26,7 +27,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({ "Failed to set custom default role due to plan RBAC restriction. Upgrade plan to set custom default org membership role." }); - const customRole = await orgRoleDAL.findOne({ slug: membershipRoleSlug, orgId }); + const customRole = await roleDAL.findOne({ slug: membershipRoleSlug, orgId }); if (!customRole) { throw new NotFoundError({ name: "UpdateOrg", 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 6ce2b22cd..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 } from "../auth/auth-type"; -import { TOrgRoleDALFactory } from "./org-role-dal"; - -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, membership } = await permissionService.getUserOrgPermission( - userId, - orgId, - actorAuthMethod, - actorOrgId - ); - return { permissions: packRules(permission.rules), membership }; - }; - - 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 ffcf4459e..5b98b44b1 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -3,19 +3,15 @@ import slugify from "@sindresorhus/slugify"; import { Knex } from "knex"; import { - ActionProjectType, + AccessScope, 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"; @@ -25,47 +21,35 @@ 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 { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; 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"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; +import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { ActorAuthMethod, ActorType, AuthMethod, AuthModeJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TReminderServiceFactory } from "../reminder/reminder-types"; +import { TRoleDALFactory } from "../role/role-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -75,8 +59,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 { TOrgRoleDALFactory } from "./org-role-dal"; +import { deleteOrgMembershipsFn } from "./org-fns"; import { TDeleteOrgMembershipDTO, TDeleteOrgMembershipsDTO, @@ -84,7 +67,6 @@ import { TFindOrgMembersByEmailDTO, TGetOrgGroupsDTO, TGetOrgMembershipDTO, - TInviteUserToOrgDTO, TListProjectMembershipsByOrgMembershipIdDTO, TResendOrgMemberInvitationDTO, TUpdateOrgDTO, @@ -100,31 +82,22 @@ type TOrgServiceFactoryDep = { folderDAL: Pick; orgDAL: TOrgDALFactory; orgBotDAL: TOrgBotDALFactory; - orgRoleDAL: TOrgRoleDALFactory; + roleDAL: TRoleDALFactory; userDAL: TUserDALFactory; groupDAL: TGroupDALFactory; projectDAL: TProjectDALFactory; identityMetadataDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; projectMembershipDAL: Pick< TProjectMembershipDALFactory, - | "findProjectMembershipsByUserId" - | "delete" - | "create" - | "find" - | "insertMany" - | "transaction" - | "findProjectMembershipsByUserIds" + "findProjectMembershipsByUserId" | "findProjectMembershipsByUserIds" >; projectKeyDAL: Pick; orgMembershipDAL: Pick< TOrgMembershipDALFactory, - | "findOrgMembershipById" - | "findOne" - | "findById" - | "findRecentInvitedMemberships" - | "updateById" - | "updateLastInvitedAtByIds" + "findOrgMembershipById" | "findRecentInvitedMemberships" | "updateLastInvitedAtByIds" >; + membershipRoleDAL: TMembershipRoleDALFactory; incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; oidcConfigDAL: Pick; @@ -136,12 +109,11 @@ type TOrgServiceFactoryDep = { TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer" >; - projectUserAdditionalPrivilegeDAL: Pick; - projectRoleDAL: Pick; - projectUserMembershipRoleDAL: Pick; projectBotService: Pick; loginService: Pick; reminderService: Pick; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; }; export type TOrgServiceFactory = ReturnType; @@ -154,7 +126,7 @@ export const orgServiceFactory = ({ folderDAL, userDAL, groupDAL, - orgRoleDAL, + roleDAL, incidentContactDAL, permissionService, smtpService, @@ -162,19 +134,20 @@ export const orgServiceFactory = ({ projectMembershipDAL, projectKeyDAL, orgMembershipDAL, - projectUserAdditionalPrivilegeDAL, tokenService, orgBotDAL, licenseService, - projectRoleDAL, samlConfigDAL, oidcConfigDAL, ldapConfigDAL, - projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, loginService, - reminderService + reminderService, + membershipRoleDAL, + membershipUserDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }: TOrgServiceFactoryDep) => { /* * Get organization details by the organization id @@ -185,7 +158,7 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission(ActorType.USER, userId, orgId, actorAuthMethod, actorOrgId); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); @@ -221,7 +194,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findAllOrgMembers(orgId); @@ -267,7 +246,7 @@ export const orgServiceFactory = ({ } if (actor === ActorType.IDENTITY) { - const workspaces = await projectDAL.findAllProjectsByIdentity(actorId); + const workspaces = await projectDAL.findIdentityProjects(actorId, orgId); return workspaces; } @@ -302,14 +281,21 @@ export const orgServiceFactory = ({ ); const createMembershipData = { - orgId, - userId: user.id, - role: OrgMembershipRole.Admin, + scopeOrgId: orgId, + scope: AccessScope.Organization, + actorUserId: user.id, status: OrgMembershipStatus.Accepted, isActive: true }; - await orgDAL.createMembership(createMembershipData, tx); + const membership = await orgDAL.createMembership(createMembershipData, tx); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); return { user, @@ -323,9 +309,15 @@ export const orgServiceFactory = ({ actorAuthMethod, orgId }: TUpgradePrivilegeSystemDTO) => { - const { membership } = await permissionService.getUserOrgPermission(actorId, orgId, actorAuthMethod, actorOrgId); + const { hasRole } = await permissionService.getOrgPermission( + ActorType.USER, + actorId, + orgId, + actorAuthMethod, + actorOrgId + ); - if (membership.role !== OrgMembershipRole.Admin) { + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Insufficient privileges - only the organization admin can upgrade the privilege system." }); @@ -531,7 +523,7 @@ export const orgServiceFactory = ({ defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({ membershipRoleSlug: defaultMembershipRoleSlug, orgId, - orgRoleDAL, + roleDAL, plan }); } @@ -601,16 +593,23 @@ export const orgServiceFactory = ({ tx ); if (userId) { - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId, - orgId: org.id, - role: OrgMembershipRole.Admin, + scope: AccessScope.Organization, + actorUserId: userId, + scopeOrgId: org.id, status: OrgMembershipStatus.Accepted, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); } await orgBotDAL.create( { @@ -659,8 +658,14 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string | undefined; }) => { - const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); - if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) { + const { hasRole } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); + if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ name: "DeleteOrganizationById", message: "Insufficient privileges" @@ -739,22 +744,32 @@ export const orgServiceFactory = ({ actorOrgId, metadata }: TUpdateOrgMembershipDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); - const foundMembership = await orgMembershipDAL.findById(membershipId); + const foundMembership = await membershipUserDAL.findOne({ + id: membershipId, + scope: AccessScope.Organization, + scopeOrgId: actorOrgId + }); if (!foundMembership) throw new NotFoundError({ message: `Organization membership with ID ${membershipId} not found` }); - if (foundMembership.orgId !== orgId) + if (foundMembership.scopeOrgId !== orgId) throw new UnauthorizedError({ message: "Updated org member doesn't belong to the organization" }); - if (foundMembership.userId === userId) + if (foundMembership.actorUserId === userId) throw new UnauthorizedError({ message: "Cannot update own organization membership" }); const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); let userRole = role; let userRoleId: string | null = null; if (role && isCustomRole) { - const customRole = await orgRoleDAL.findOne({ slug: role, orgId }); + const customRole = await roleDAL.findOne({ slug: role, orgId }); if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" }); const plan = await licenseService.getPlan(orgId); @@ -767,17 +782,33 @@ export const orgServiceFactory = ({ userRoleId = customRole.id; } const membership = await orgDAL.transaction(async (tx) => { - const [updatedOrgMembership] = await orgDAL.updateMembership( - { id: membershipId, orgId }, - { role: userRole, roleId: userRoleId, isActive } - ); + // this is because if isActive is undefined then this would fail due to knexjs error + const [updatedOrgMembership] = + typeof isActive === "undefined" + ? [foundMembership] + : await orgDAL.updateMembership( + { id: membershipId, scopeOrgId: orgId, scope: AccessScope.Organization }, + { isActive }, + tx + ); + if (userRole) { + await membershipRoleDAL.delete({ membershipId }, tx); + await membershipRoleDAL.create( + { + membershipId, + role: userRole, + customRoleId: userRoleId + }, + tx + ); + } if (metadata) { - await identityMetadataDAL.delete({ userId: updatedOrgMembership.userId, orgId }, tx); + await identityMetadataDAL.delete({ userId: updatedOrgMembership.actorUserId, orgId }, tx); if (metadata.length) { await identityMetadataDAL.insertMany( metadata.map(({ key, value }) => ({ - userId: updatedOrgMembership.userId, + userId: updatedOrgMembership.actorUserId as string, orgId, key, value @@ -809,8 +840,9 @@ export const orgServiceFactory = ({ const org = await orgDAL.findOrgById(orgId); const [inviteeOrgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId, - [`${TableName.OrgMembership}.id` as "id"]: membershipId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization, + [`${TableName.Membership}.id` as "id"]: membershipId }); if (inviteeOrgMembership.status !== OrgMembershipStatus.Invited) { @@ -821,7 +853,7 @@ export const orgServiceFactory = ({ const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: inviteeOrgMembership.userId, + userId: inviteeOrgMembership.actorUserId as string, orgId }); @@ -849,366 +881,13 @@ export const orgServiceFactory = ({ } }); - await orgMembershipDAL.updateById(inviteeOrgMembership.id, { + await membershipUserDAL.updateById(inviteeOrgMembership.id, { lastInvitedAt: new Date() }); 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.OrgMembership}.orgId` as "orgId"]: orgId, - [`${TableName.OrgMembership}.userId` as "userId"]: 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 orgRoleDAL.findOne({ slug: organizationRoleSlug, orgId }); - if (!customRole) { - throw new NotFoundError({ - name: "InviteUser", - message: `Custom organization role with slug '${orgRole}' not found` - }); - } - roleId = customRole.id; - } - - await orgDAL.createMembership( - { - userId: inviteeUser.id, - inviteEmail: inviteeEmail, - orgId, - role: orgRole, - status: OrgMembershipStatus.Invited, - isActive: true, - 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 projectMembershipDAL.find( - { - projectId: project.id, - $in: { userId: userIds } - }, - { tx } - ); - const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId); - 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 @@ -1222,9 +901,10 @@ export const orgServiceFactory = ({ } const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: user.id, + [`${TableName.Membership}.actorUserId` as "actorUserId"]: user.id, + scope: AccessScope.Organization, status: OrgMembershipStatus.Invited, - [`${TableName.OrgMembership}.orgId` as "orgId"]: orgId + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: orgId }); if (!orgMembership) @@ -1237,7 +917,7 @@ export const orgServiceFactory = ({ await tokenService.validateTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, userId: user.id, - orgId: orgMembership.orgId, + orgId: orgMembership.scopeOrgId, code }); @@ -1249,16 +929,17 @@ export const orgServiceFactory = ({ // this means user has already completed signup process // isAccepted is set true when keys are exchanged await orgDAL.updateMembershipById(orgMembership.id, { - orgId, + scopeOrgId: orgId, status: OrgMembershipStatus.Accepted }); await licenseService.updateSubscriptionOrgMemberCount(orgId); return { user }; } + const membershipRole = await membershipRoleDAL.findOne({ membershipId: orgMembership.id }); if ( organization.authEnforced && - !(organization.bypassOrgAuthEnabled && orgMembership.role === OrgMembershipRole.Admin) + !(organization.bypassOrgAuthEnabled && membershipRole.role === OrgMembershipRole.Admin) ) { return { user }; } @@ -1307,19 +988,27 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); - const deletedMembership = await deleteOrgMembershipFn({ - orgMembershipId: membershipId, + const [deletedMembership] = await deleteOrgMembershipsFn({ + orgMembershipIds: [membershipId], orgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL, + membershipRoleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); return deletedMembership; @@ -1332,7 +1021,13 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipsDTO) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); if (membershipIds.includes(userId)) { @@ -1343,12 +1038,14 @@ export const orgServiceFactory = ({ orgMembershipIds: membershipIds, orgId, orgDAL, - projectMembershipDAL, - projectUserAdditionalPrivilegeDAL, projectKeyDAL, userAliasDAL, licenseService, - userId + userId, + membershipUserDAL, + membershipRoleDAL, + userGroupMembershipDAL, + additionalPrivilegeDAL }); return deletedMemberships; @@ -1385,7 +1082,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); const incidentContacts = await incidentContactDAL.findByOrgId(orgId); return incidentContacts; @@ -1398,7 +1101,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); if (doesIncidentContactExist) { @@ -1419,7 +1128,13 @@ export const orgServiceFactory = ({ actorAuthMethod: ActorAuthMethod, actorOrgId: string | undefined ) => { - const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission( + ActorType.USER, + userId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); const incidentContact = await incidentContactDAL.deleteById(id, orgId); @@ -1440,17 +1155,17 @@ export const orgServiceFactory = ({ await Promise.all( invitedUsers.map(async (invitedUser) => { - let org = orgCache[invitedUser.orgId]; + let org = orgCache[invitedUser.scopeOrgId]; if (!org) { - org = await orgDAL.findById(invitedUser.orgId); - orgCache[invitedUser.orgId] = org; + org = await orgDAL.findById(invitedUser.scopeOrgId); + orgCache[invitedUser.scopeOrgId] = org; } - if (!org || !invitedUser.userId) return; + if (!org || !invitedUser.actorUserId) return; const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: invitedUser.userId, + userId: invitedUser.actorUserId, orgId: org.id }); @@ -1488,7 +1203,6 @@ export const orgServiceFactory = ({ findAllOrgMembers, findAllOrganizationOfUser, findIdentityOrganization, - inviteUserToOrganization, verifyUserToOrg, updateOrg, findOrgMembersByUsername, diff --git a/backend/src/services/project-bot/project-bot-dal.ts b/backend/src/services/project-bot/project-bot-dal.ts index ecb23f78b..2f1863ad6 100644 --- a/backend/src/services/project-bot/project-bot-dal.ts +++ b/backend/src/services/project-bot/project-bot-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TProjectBots, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TProjectBots, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -44,12 +44,13 @@ export const projectBotDALFactory = (db: TDbClient) => { const findProjectUserWorkspaceKey = async (projectId: string) => { try { const doc = await db - .replicaNode()(TableName.ProjectMembership) - .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) - .where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId) + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId` as "projectId", projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) .where(`${TableName.Users}.isGhost` as "isGhost", false) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) - .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(TableName.ProjectKeys, `${TableName.Membership}.actorUserId`, `${TableName.ProjectKeys}.receiverId`) + .where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index bb91b9c85..07d061e95 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TProjectKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TProjectKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols } from "@app/lib/knex"; @@ -34,11 +34,12 @@ export const projectKeyDALFactory = (db: TDbClient) => { const findAllProjectUserPubKeys = async (projectId: string, tx?: Knex) => { try { - const pubKeys = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + const pubKeys = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scopeProjectId` as "scopeProjectId", projectId) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) - .select("userId", "publicKey"); + .select(db.ref("userId").withSchema(TableName.Users), "publicKey"); return pubKeys; } catch (error) { throw new DatabaseError({ error, name: "Find all workspace pub keys" }); diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index a884d25bc..7faa11f2b 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -1,25 +1,25 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { AccessScope, ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TProjectKeyDALFactory } from "./project-key-dal"; import { TGetLatestProjectKeyDTO, TUploadProjectKeyDTO } from "./project-key-types"; type TProjectKeyServiceFactoryDep = { permissionService: TPermissionServiceFactory; projectKeyDAL: TProjectKeyDALFactory; - projectMembershipDAL: TProjectMembershipDALFactory; + membershipUserDAL: TMembershipUserDALFactory; }; export type TProjectKeyServiceFactory = ReturnType; export const projectKeyServiceFactory = ({ projectKeyDAL, - projectMembershipDAL, + membershipUserDAL, permissionService }: TProjectKeyServiceFactoryDep) => { const uploadProjectKeys = async ({ @@ -42,9 +42,10 @@ export const projectKeyServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const receiverMembership = await projectMembershipDAL.findOne({ - userId: receiverId, - projectId + const receiverMembership = await membershipUserDAL.findOne({ + actorUserId: receiverId, + scopeProjectId: projectId, + scope: AccessScope.Project }); if (!receiverMembership) throw new BadRequestError({ diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index dd503c2a3..fe3b57226 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -1,15 +1,13 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; +import { AccessScope, TableName, TMemberships, TUserEncryptionKeys } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TProjectMembershipDALFactory = ReturnType; export const projectMembershipDALFactory = (db: TDbClient) => { - const projectMemberOrm = ormify(db, TableName.ProjectMembership); - // special query const findAllProjectMembers = async ( projectId: string, @@ -17,18 +15,17 @@ export const projectMembershipDALFactory = (db: TDbClient) => { ) => { try { const docs = await db - .replicaNode()(TableName.ProjectMembership) - .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) - .join(TableName.OrgMembership, (qb) => { - qb.on(`${TableName.Users}.id`, "=", `${TableName.OrgMembership}.userId`).andOn( - `${TableName.OrgMembership}.orgId`, - "=", - `${TableName.Project}.orgId` - ); + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId }) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .join(db(TableName.Membership).as("orgMembership"), (qb) => { + qb.on(`${TableName.Users}.id`, "=", `orgMembership.actorUserId`) + .andOn(`orgMembership.scopeOrgId`, "=", `${TableName.Project}.orgId`) + .andOn("orgMembership.scope", db.raw("?", [AccessScope.Organization])); }) - .where((qb) => { if (filter.usernames) { void qb.whereIn("username", filter.usernames); @@ -37,69 +34,46 @@ export const projectMembershipDALFactory = (db: TDbClient) => { void qb.where("username", filter.username); } if (filter.id) { - void qb.where(`${TableName.ProjectMembership}.id`, filter.id); + void qb.where(`${TableName.Membership}.id`, filter.id); } if (filter.roles && filter.roles.length > 0) { void qb.whereExists((subQuery) => { void subQuery .select("role") - .from(TableName.ProjectUserMembershipRole) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectRoles}.id`, - `${TableName.ProjectUserMembershipRole}.customRoleId` - ) - .whereRaw("??.?? = ??.??", [ - TableName.ProjectUserMembershipRole, - "projectMembershipId", - TableName.ProjectMembership, - "id" - ]) + .from(TableName.MembershipRole) + .leftJoin(TableName.Role, `${TableName.Role}.id`, `${TableName.MembershipRole}.customRoleId`) + .whereRaw("??.?? = ??.??", [TableName.MembershipRole, "membershipId", TableName.Membership, "id"]) .where((subQb) => { void subQb - .whereIn(`${TableName.ProjectUserMembershipRole}.role`, filter.roles as string[]) - .orWhereIn(`${TableName.ProjectRoles}.slug`, filter.roles as string[]); + .whereIn(`${TableName.MembershipRole}.role`, filter.roles as string[]) + .orWhereIn(`${TableName.Role}.slug`, filter.roles as string[]); }); }); } }) - .join( - TableName.UserEncryptionKey, - `${TableName.UserEncryptionKey}.userId`, - `${TableName.Users}.id` - ) - .join( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.ProjectMembership), - db.ref("createdAt").withSchema(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Membership), + db.ref("createdAt").withSchema(TableName.Membership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), - db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), - db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), - db.ref("isActive").withSchema(TableName.OrgMembership) + db.ref("isActive").withSchema("orgMembership") ) .where({ isGhost: false }) .orderBy(`${TableName.Users}.username` as "username"); @@ -111,7 +85,6 @@ export const projectMembershipDALFactory = (db: TDbClient) => { firstName, username, lastName, - publicKey, isGhost, id, userId, @@ -128,7 +101,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => { firstName, lastName, id: userId, - publicKey, + // akhilmhdh: if we do user encryption based join this would fail for scim user who haven't logged in yet + // public key is not used anymore as well + publicKey: "", isGhost, isOrgMembershipActive: isActive }, @@ -184,9 +159,11 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + const ghostUser = await (tx || db.replicaNode())(TableName.Membership) + .where({ [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId }) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) .where({ isGhost: true }) .first(); @@ -200,21 +177,24 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findMembershipsByUsername = async (projectId: string, usernames: string[]) => { try { const members = await db - .replicaNode()(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scopeProjectId` as "scopeProjectId"]: projectId }) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .join( TableName.UserEncryptionKey, `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) .select( - selectAllTableCols(TableName.ProjectMembership), + selectAllTableCols(TableName.Membership), db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("username").withSchema(TableName.Users) ) .whereIn("username", usernames) .where({ isGhost: false }); + return members.map(({ userId, username, ...data }) => ({ ...data, user: { id: userId, username } @@ -227,45 +207,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectMembershipsByUserId = async (orgId: string, userId: string) => { try { const docs = await db - .replicaNode()(TableName.ProjectMembership) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.id`, userId) .where(`${TableName.Project}.orgId`, orgId) - .join( - TableName.UserEncryptionKey, - `${TableName.UserEncryptionKey}.userId`, - `${TableName.Users}.id` - ) - .join( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), - db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), - db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("projectId").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project) @@ -274,22 +242,21 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const members = sqlNestRelationships({ data: docs, - parentMapper: ({ - email, - firstName, - username, - lastName, - publicKey, - isGhost, - id, - projectId, - projectName, - projectType - }) => ({ + parentMapper: ({ email, firstName, username, lastName, isGhost, id, projectId, projectName, projectType }) => ({ id, userId, projectId, - user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }, + user: { + email, + username, + firstName, + lastName, + id: userId, + isGhost, + // akhilmhdh: if we do user encryption based join this would fail for scim user who haven't logged in yet + // public key is not used anymore as well + publicKey: "" + }, project: { id: projectId, name: projectName, @@ -336,9 +303,11 @@ export const projectMembershipDALFactory = (db: TDbClient) => { const findProjectMembershipsByUserIds = async (orgId: string, userIds: string[]) => { try { const docs = await db - .replicaNode()(TableName.ProjectMembership) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .whereIn(`${TableName.Users}.id`, userIds) .where(`${TableName.Project}.orgId`, orgId) .join( @@ -346,18 +315,10 @@ export const projectMembershipDALFactory = (db: TDbClient) => { `${TableName.UserEncryptionKey}.userId`, `${TableName.Users}.id` ) - .join( - TableName.ProjectUserMembershipRole, - `${TableName.ProjectUserMembershipRole}.projectMembershipId`, - `${TableName.ProjectMembership}.id` - ) - .leftJoin( - TableName.ProjectRoles, - `${TableName.ProjectUserMembershipRole}.customRoleId`, - `${TableName.ProjectRoles}.id` - ) + .join(TableName.MembershipRole, `${TableName.MembershipRole}.membershipId`, `${TableName.Membership}.id`) + .leftJoin(TableName.Role, `${TableName.MembershipRole}.customRoleId`, `${TableName.Role}.id`) .select( - db.ref("id").withSchema(TableName.ProjectMembership), + db.ref("id").withSchema(TableName.Membership), db.ref("isGhost").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), @@ -365,16 +326,16 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), - db.ref("role").withSchema(TableName.ProjectUserMembershipRole), - db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("membershipRoleId"), - db.ref("customRoleId").withSchema(TableName.ProjectUserMembershipRole), - db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), - db.ref("temporaryMode").withSchema(TableName.ProjectUserMembershipRole), - db.ref("isTemporary").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole), + db.ref("role").withSchema(TableName.MembershipRole), + db.ref("id").withSchema(TableName.MembershipRole).as("membershipRoleId"), + db.ref("customRoleId").withSchema(TableName.MembershipRole), + db.ref("name").withSchema(TableName.Role).as("customRoleName"), + db.ref("slug").withSchema(TableName.Role).as("customRoleSlug"), + db.ref("temporaryMode").withSchema(TableName.MembershipRole), + db.ref("isTemporary").withSchema(TableName.MembershipRole), + db.ref("temporaryRange").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessStartTime").withSchema(TableName.MembershipRole), + db.ref("temporaryAccessEndTime").withSchema(TableName.MembershipRole), db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("projectId").withSchema(TableName.Project), db.ref("type").as("projectType").withSchema(TableName.Project) @@ -444,7 +405,6 @@ export const projectMembershipDALFactory = (db: TDbClient) => { }; return { - ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByUsername, diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 301ce1bc0..b0949d550 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -1,64 +1,51 @@ /* eslint-disable no-await-in-loop */ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; +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"; type TProjectMembershipServiceFactoryDep = { permissionService: Pick< TPermissionServiceFactory, - "getProjectPermission" | "getProjectPermissionByRole" | "invalidateProjectPermissionCache" + "getProjectPermission" | "getProjectPermissionByRoles" | "invalidateProjectPermissionCache" >; smtpService: TSmtpService; - projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; - projectUserMembershipRoleDAL: Pick; - userDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: Pick; + userDAL: Pick; userGroupMembershipDAL: TUserGroupMembershipDALFactory; - projectRoleDAL: Pick; - orgDAL: Pick; projectDAL: Pick; projectKeyDAL: Pick; licenseService: Pick; - projectUserAdditionalPrivilegeDAL: Pick; + additionalPrivilegeDAL: Pick; secretReminderRecipientsDAL: Pick; groupProjectDAL: TGroupProjectDALFactory; notificationService: Pick; @@ -69,19 +56,17 @@ export type TProjectMembershipServiceFactory = ReturnType { const getProjectMemberships = async ({ actorId, @@ -150,29 +135,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; - }; - const addUsersToProject = async ({ projectId, actorId, @@ -194,48 +156,58 @@ export const projectMembershipServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); - const orgMembers = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId, + const orgMembers = await membershipUserDAL.find({ + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: project.orgId, + scope: AccessScope.Organization, $in: { - [`${TableName.OrgMembership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId) + [`${TableName.Membership}.id` as "id"]: members.map(({ orgMembershipId }) => orgMembershipId) } }); + if (orgMembers.length !== members.length) throw new BadRequestError({ message: "Some users are not part of org" }); - const existingMembers = await projectMembershipDAL.find({ - projectId, - $in: { userId: orgMembers.map(({ userId }) => userId).filter(Boolean) } + 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 projectMembershipDAL.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 @@ -246,8 +218,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", @@ -259,7 +231,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` @@ -269,164 +241,6 @@ export const projectMembershipServiceFactory = ({ return orgMembers; }; - const updateProjectMembership = async ({ - actorId, - actor, - actorOrgId, - actorAuthMethod, - projectId, - membershipId, - roles - }: TUpdateProjectMembershipDTO) => { - const { permission, membership } = 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" }); - } - - for await (const { role: requestedRoleChange } of roles) { - const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( - requestedRoleChange, - projectId - ); - - const permissionBoundary = validatePrivilegeChangeOperation( - membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.GrantPrivileges, - ProjectPermissionSub.Member, - permission, - rolePermission - ); - if (!permissionBoundary.isValid) - throw new PermissionBoundaryError({ - message: constructPermissionErrorMessage( - `Failed to change role ${requestedRoleChange}`, - membership.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) - ); - 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 projectRoleDAL.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 projectMembershipDAL.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, @@ -478,20 +292,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) } @@ -564,11 +379,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 ); @@ -582,10 +397,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 ) @@ -603,11 +419,8 @@ export const projectMembershipServiceFactory = ({ return { getProjectMemberships, getProjectMembershipByUsername, - updateProjectMembership, deleteProjectMemberships, - deleteProjectMembership, // TODO: Remove this addUsersToProject, - leaveProject, - getProjectMembershipById + leaveProject }; }; diff --git a/backend/src/services/project-membership/project-user-membership-role-dal.ts b/backend/src/services/project-membership/project-user-membership-role-dal.ts deleted file mode 100644 index b1cb55b9b..000000000 --- a/backend/src/services/project-membership/project-user-membership-role-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TProjectUserMembershipRoleDALFactory = ReturnType; - -export const projectUserMembershipRoleDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.ProjectUserMembershipRole); - return orm; -}; diff --git a/backend/src/services/project-role/project-role-dal.ts b/backend/src/services/project-role/project-role-dal.ts deleted file mode 100644 index 942fefa11..000000000 --- a/backend/src/services/project-role/project-role-dal.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TProjectRoleDALFactory = ReturnType; - -export const projectRoleDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectRoles); 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 f30da21f5..000000000 --- a/backend/src/services/project-role/project-role-service.ts +++ /dev/null @@ -1,286 +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, membership } = await permissionService.getProjectPermission({ - actor: ActorType.USER, - actorId: userId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - // just to satisfy ts - if (!("roles" in membership)) throw new BadRequestError({ message: "Service token not allowed" }); - - 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), membership, assumedPrivilegeDetails }; - }; - - return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug }; -}; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index d64977f8b..2abdebdbc 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, ProjectsSchema, ProjectType, ProjectUpgradeStatus, @@ -24,9 +25,11 @@ export const projectDALFactory = (db: TDbClient) => { const findIdentityProjects = async (identityId: string, orgId: string, projectType?: ProjectType) => { try { - const workspaces = await db(TableName.IdentityProjectMembership) - .where({ identityId }) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + const workspaces = await db + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.actorIdentityId`, identityId) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) .andWhere((qb) => { if (projectType) { @@ -74,11 +77,23 @@ export const projectDALFactory = (db: TDbClient) => { const findUserProjects = async (userId: string, orgId: string, projectType?: ProjectType) => { try { - const workspaces = await db - .replicaNode()(TableName.ProjectMembership) - .where({ userId }) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + const userGroupSubquery = db + .replicaNode()(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, orgId) + .where(`${TableName.UserGroupMembership}.userId`, userId) + .select(db.ref("id").withSchema(TableName.Groups)); + + const projects = await db + .replicaNode()(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + void qb + .where(`${TableName.Membership}.actorUserId`, userId) + .orWhereIn(`${TableName.Membership}.actorGroupId`, userGroupSubquery); + }) .andWhere((qb) => { if (projectType) { void qb.where(`${TableName.Project}.type`, projectType); @@ -97,36 +112,8 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Environment}.position`, order: "asc" } ]); - const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId"); - - const groupWorkspaces = await db(TableName.GroupProjectMembership) - .whereIn("groupId", groups) - .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) - .where(`${TableName.Project}.orgId`, orgId) - .andWhere((qb) => { - if (projectType) { - void qb.where(`${TableName.Project}.type`, projectType); - } - }) - .whereNotIn( - `${TableName.Project}.id`, - workspaces.map(({ id }) => id) - ) - .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) - .select( - selectAllTableCols(TableName.Project), - db.ref("id").withSchema(TableName.Project).as("_id"), - db.ref("id").withSchema(TableName.Environment).as("envId"), - db.ref("slug").withSchema(TableName.Environment).as("envSlug"), - db.ref("name").withSchema(TableName.Environment).as("envName") - ) - .orderBy([ - { column: `${TableName.Project}.name`, order: "asc" }, - { column: `${TableName.Environment}.position`, order: "asc" } - ]); - - const nestedWorkspaces = sqlNestRelationships({ - data: workspaces.concat(groupWorkspaces), + const formattedProjects = sqlNestRelationships({ + data: projects, key: "id", parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), childrenMapper: [ @@ -142,7 +129,7 @@ export const projectDALFactory = (db: TDbClient) => { ] }); - return nestedWorkspaces.map((workspace) => ({ + return formattedProjects.map((workspace) => ({ ...workspace, organization: workspace.orgId })); @@ -153,9 +140,10 @@ export const projectDALFactory = (db: TDbClient) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => { try { - const ghostUser = await (tx || db.replicaNode())(TableName.ProjectMembership) - .where({ projectId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + const ghostUser = await (tx || db.replicaNode())(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeProjectId`, projectId) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .select(selectAllTableCols(TableName.Users)) .where({ isGhost: true }) .first(); @@ -177,54 +165,6 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => { - try { - const workspaces = await db - .replicaNode()(TableName.IdentityProjectMembership) - .where({ identityId }) - .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) - .andWhere((qb) => { - if (projectType) { - void qb.where(`${TableName.Project}.type`, projectType); - } - }) - .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) - .select( - selectAllTableCols(TableName.Project), - db.ref("id").withSchema(TableName.Project).as("_id"), - db.ref("id").withSchema(TableName.Environment).as("envId"), - db.ref("slug").withSchema(TableName.Environment).as("envSlug"), - db.ref("name").withSchema(TableName.Environment).as("envName") - ) - .orderBy("createdAt", "asc", "last"); - - const nestedWorkspaces = sqlNestRelationships({ - data: workspaces, - key: "id", - parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), - childrenMapper: [ - { - key: "envId", - label: "environments" as const, - mapper: ({ envId: id, envSlug: slug, envName: name }) => ({ - id, - slug, - name - }) - } - ] - }); - - // We need to add the organization field, as it's required for one of our API endpoint responses. - return nestedWorkspaces.map((workspace) => ({ - ...workspace, - organization: workspace.orgId - })); - } catch (error) { - throw new DatabaseError({ error, name: "Find all projects by identity" }); - } - }; - const findProjectById = async (id: string) => { try { const workspaces = await db @@ -402,19 +342,26 @@ export const projectDALFactory = (db: TDbClient) => { projectIds?: string[]; }) => { const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto; - - const userMembershipSubquery = db(TableName.ProjectMembership).where({ userId: dto.actorId }).select("projectId"); - const groups = db(TableName.UserGroupMembership).where({ userId: dto.actorId }).select("groupId"); - const groupMembershipSubquery = db(TableName.GroupProjectMembership).whereIn("groupId", groups).select("projectId"); - - const identityMembershipSubQuery = db(TableName.IdentityProjectMembership) - .where({ identityId: dto.actorId }) - .select("projectId"); + const groupMembershipSubquery = db(TableName.Groups) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`) + .where(`${TableName.Groups}.orgId`, dto.orgId) + .where(`${TableName.UserGroupMembership}.userId`, dto.actorId) + .select(db.ref("id").withSchema(TableName.Groups)); + const membershipSubQuery = db(TableName.Membership) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where((qb) => { + if (dto.actor === ActorType.IDENTITY) { + void qb.where(`${TableName.Membership}.actorIdentityId`, dto.actorId); + } else { + void qb + .where(`${TableName.Membership}.actorUserId`, dto.actorId) + .orWhereIn(`${TableName.Membership}.actorGroupId`, groupMembershipSubquery); + } + }) + .select("scopeProjectId"); // Get the SQL strings for the subqueries - const userMembershipSql = userMembershipSubquery.toQuery(); - const groupMembershipSql = groupMembershipSubquery.toQuery(); - const identityMembershipSql = identityMembershipSubQuery.toQuery(); + const membershipSQL = membershipSubQuery.toQuery(); const query = db .replicaNode()(TableName.Project) @@ -422,26 +369,15 @@ export const projectDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.Project)) .select(db.raw("COUNT(*) OVER() AS count")) .select<(TProjects & { isMember: boolean; count: number })[]>( - dto.actor === ActorType.USER - ? db.raw( - ` - CASE - WHEN ${TableName.Project}.id IN (?) THEN TRUE - WHEN ${TableName.Project}.id IN (?) THEN TRUE - ELSE FALSE - END as "isMember" - `, - [db.raw(userMembershipSql), db.raw(groupMembershipSql)] - ) - : db.raw( - ` - CASE - WHEN ${TableName.Project}.id IN (?) THEN TRUE - ELSE FALSE - END as "isMember" - `, - [db.raw(identityMembershipSql)] - ) + db.raw( + ` + CASE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + ELSE FALSE + END as "isMember" + `, + [db.raw(membershipSQL)] + ) ) .limit(limit) .offset(offset); @@ -495,7 +431,6 @@ export const projectDALFactory = (db: TDbClient) => { findUserProjects, findIdentityProjects, setProjectUpgradeStatus, - findAllProjectsByIdentity, findProjectGhostUser, findProjectById, findProjectByFilter, diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index e557b7c96..30ec6d29f 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -1,5 +1,6 @@ /* eslint-disable no-await-in-loop */ import { + AccessScope, IntegrationAuthsSchema, ProjectMembershipRole, ProjectUpgradeStatus, @@ -29,13 +30,13 @@ import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretVersionDALFactory } from "../secret/secret-version-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -55,13 +56,13 @@ type TProjectQueueFactoryDep = { secretApprovalSecretDAL: Pick; projectBotDAL: Pick; orgService: Pick; - projectMembershipDAL: Pick; - projectUserMembershipRoleDAL: Pick; integrationAuthDAL: TIntegrationAuthDALFactory; userDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; orgDAL: Pick; + membershipUserDAL: TMembershipUserDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; }; export const projectQueueFactory = ({ @@ -79,8 +80,8 @@ export const projectQueueFactory = ({ orgDAL, projectDAL, orgService, - projectMembershipDAL, - projectUserMembershipRoleDAL + membershipUserDAL, + membershipRoleDAL }: TProjectQueueFactoryDep) => { const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => { await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, { @@ -227,17 +228,16 @@ export const projectQueueFactory = ({ ); // Create a membership for the ghost user - const projectMembership = await projectMembershipDAL.create( + const projectMembership = await membershipUserDAL.create( { - projectId: project.id, - userId: ghostUser.user.id + scopeProjectId: project.id, + scope: AccessScope.Project, + actorUserId: ghostUser.user.id, + scopeOrgId: project.orgId }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); + await membershipRoleDAL.create({ membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, tx); // If a bot already exists, delete it if (existingBot) { @@ -272,8 +272,9 @@ export const projectQueueFactory = ({ for (const key of existingProjectKeys) { const user = await userDAL.findUserEncKeyByUserId(key.receiverId); const [orgMembership] = await orgDAL.findMembership({ - [`${TableName.OrgMembership}.userId` as "userId"]: key.receiverId, - [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId + [`${TableName.Membership}.actorUserId` as "actorUserId"]: key.receiverId, + [`${TableName.Membership}.scopeOrgId` as "scopeOrgId"]: project.orgId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Organization }); if (!user) { diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 8dbba2b4f..58b3c5395 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -3,6 +3,7 @@ import { PackRule, unpackRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import { + AccessScope, ActionProjectType, ProjectMembershipRole, ProjectType, @@ -49,11 +50,11 @@ import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; import { expandInternalCa } from "../certificate-authority/certificate-authority-fns"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; -import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; -import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; -import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipGroupDALFactory } from "../membership-group/membership-group-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns"; import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; @@ -65,10 +66,9 @@ import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; -import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { getPredefinedRoles } from "../project-role/project-role-fns"; import { TReminderServiceFactory } from "../reminder/reminder-types"; +import { TRoleDALFactory } from "../role/role-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { fnDeleteProjectSecretReminders } from "../secret/secret-fns"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; @@ -125,7 +125,6 @@ export const DEFAULT_PROJECT_ENVS = [ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; - identityProjectDAL: Pick; projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; @@ -134,13 +133,11 @@ type TProjectServiceFactoryDep = { secretDAL: Pick; secretV2BridgeDAL: Pick; projectEnvDAL: Pick; - identityOrgMembershipDAL: TIdentityOrgDALFactory; - identityProjectMembershipRoleDAL: Pick; - projectMembershipDAL: Pick< - TProjectMembershipDALFactory, - "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" - >; - groupProjectDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipGroupDAL: Pick; + membershipIdentityDAL: Pick; + membershipRoleDAL: Pick; projectSlackConfigDAL: Pick< TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create" | "delete" @@ -154,7 +151,6 @@ type TProjectServiceFactoryDep = { TMicrosoftTeamsIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails" >; - projectUserMembershipRoleDAL: Pick; pkiSubscriberDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; @@ -172,7 +168,7 @@ type TProjectServiceFactoryDep = { smtpService: Pick; orgDAL: Pick; keyStore: Pick; - projectRoleDAL: Pick; + roleDAL: Pick; kmsService: Pick< TKmsServiceFactory, | "updateProjectSecretManagerKmsKey" @@ -201,20 +197,15 @@ export const projectServiceFactory = ({ orgDAL, userDAL, folderDAL, - identityOrgMembershipDAL, projectMembershipDAL, projectEnvDAL, licenseService, - projectUserMembershipRoleDAL, - projectRoleDAL, certificateAuthorityDAL, certificateDAL, certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, pkiSubscriberDAL, - identityProjectDAL, - identityProjectMembershipRoleDAL, sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateDAL, @@ -228,10 +219,13 @@ export const projectServiceFactory = ({ slackIntegrationDAL, microsoftTeamsIntegrationDAL, projectTemplateService, - groupProjectDAL, smtpService, reminderService, - notificationService + notificationService, + membershipIdentityDAL, + membershipUserDAL, + membershipRoleDAL, + roleDAL }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -345,7 +339,7 @@ export const projectServiceFactory = ({ tx ); } - await projectRoleDAL.insertMany( + await roleDAL.insertMany( projectTemplate.packedRoles.map((role) => ({ ...role, permissions: JSON.stringify(role.permissions), @@ -374,15 +368,17 @@ export const projectServiceFactory = ({ } // Create a membership for the user - const userProjectMembership = await projectMembershipDAL.create( + const userProjectMembership = await membershipUserDAL.create( { - projectId: project.id, - userId: user.id + scopeProjectId: project.id, + actorUserId: user.id, + scope: AccessScope.Project, + scopeOrgId: project.orgId }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, + await membershipRoleDAL.create( + { membershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, tx ); } @@ -390,10 +386,11 @@ export const projectServiceFactory = ({ // If the project is being created by an identity, add the identity to the project as an admin else if (actor === ActorType.IDENTITY) { // Find identity org membership - const identityOrgMembership = await identityOrgMembershipDAL.findOne( + const identityOrgMembership = await membershipIdentityDAL.findOne( { - identityId: actorId, - orgId: project.orgId + actorIdentityId: actorId, + scopeOrgId: project.orgId, + scope: AccessScope.Organization }, tx ); @@ -405,17 +402,19 @@ export const projectServiceFactory = ({ }); } - const identityProjectMembership = await identityProjectDAL.create( + const identityProjectMembership = await membershipIdentityDAL.create( { - identityId: actorId, - projectId: project.id + actorIdentityId: actorId, + scopeProjectId: project.id, + scope: AccessScope.Project, + scopeOrgId: project.orgId }, tx ); - await identityProjectMembershipRoleDAL.create( + await membershipRoleDAL.create( { - projectMembershipId: identityProjectMembership.id, + membershipId: identityProjectMembership.id, role: ProjectMembershipRole.Admin }, tx @@ -459,8 +458,11 @@ export const projectServiceFactory = ({ const deletedProject = await projectDAL.transaction(async (tx) => { // delete these so that project custom roles can be deleted in cascade effect // direct deletion of project without these will cause fk error - await projectMembershipDAL.delete({ projectId: project.id }, tx); - await groupProjectDAL.delete({ projectId: project.id }, tx); + // this will clean up all memberships + await membershipUserDAL.delete( + { scopeOrgId: project.orgId, scopeProjectId: project.id, scope: AccessScope.Project }, + tx + ); const delProject = await projectDAL.deleteById(project.id, tx); const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id, tx).catch(() => null); // akhilmhdh: before removing those kms checking any other project uses it @@ -511,7 +513,8 @@ export const projectServiceFactory = ({ : await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { - const { permission } = await permissionService.getUserOrgPermission( + const { permission } = await permissionService.getOrgPermission( + actor, actorId, actorOrgId, actorAuthMethod, @@ -520,13 +523,13 @@ export const projectServiceFactory = ({ // `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); - const customRoles = await projectRoleDAL.find({ + const customRoles = await roleDAL.find({ $in: { projectId: workspaces.map((workspace) => workspace.id) } }); - const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId); + const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId as string); const workspacesWithRoles = await Promise.all( workspaces.map(async (workspace) => { @@ -1346,7 +1349,7 @@ export const projectServiceFactory = ({ }; const getProjectKmsKeys = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetProjectKmsKey) => { - const { membership } = await permissionService.getProjectPermission({ + await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -1355,10 +1358,6 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.Any }); - if (!membership) { - throw new ForbiddenRequestError({ message: "You are not a member of this project" }); - } - const kmsKeyId = await kmsService.getProjectSecretManagerKmsKeyId(projectId); const kmsKey = await kmsService.getKmsById(kmsKeyId); @@ -1875,7 +1874,7 @@ export const projectServiceFactory = ({ .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) .map((el) => el.user.email!); if (filteredProjectMembers.length === 0) { - const customRolesWithMemberCreate = await projectRoleDAL.find({ projectId }); + const customRolesWithMemberCreate = await roleDAL.find({ projectId }); const customRoleSlugsCanCreate = customRolesWithMemberCreate .filter((role) => { try { diff --git a/backend/src/services/role/namespace/namespace-role-factory.ts b/backend/src/services/role/namespace/namespace-role-factory.ts new file mode 100644 index 000000000..87921c25b --- /dev/null +++ b/backend/src/services/role/namespace/namespace-role-factory.ts @@ -0,0 +1,47 @@ +import { AccessScope } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError } from "@app/lib/errors"; + +import { TRoleScopeFactory } from "../role-types"; + +type TNamespaceRoleScopeFactoryDep = { + permissionService: Pick; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const newNamespaceRoleFactory = (_dto: TNamespaceRoleScopeFactoryDep): TRoleScopeFactory => { + const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async () => {}; + + const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async () => {}; + + const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async () => {}; + + const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async () => {}; + + const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async () => {}; + + const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async () => {}; + + const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const isCustomRole: TRoleScopeFactory["isCustomRole"] = () => false; + + const getPredefinedRoles: TRoleScopeFactory["getPredefinedRoles"] = async () => []; + + return { + onCreateRoleGuard, + onUpdateRoleGuard, + onDeleteRoleGuard, + onListRoleGuard, + onGetRoleByIdGuard, + onGetRoleBySlugGuard, + getScopeField, + isCustomRole, + getPredefinedRoles + }; +}; diff --git a/backend/src/services/role/org/org-role-factory.ts b/backend/src/services/role/org/org-role-factory.ts new file mode 100644 index 000000000..50ffa5e43 --- /dev/null +++ b/backend/src/services/role/org/org-role-factory.ts @@ -0,0 +1,160 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope } 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 } from "@app/lib/errors"; +import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; +import { isCustomOrgRole } from "@app/services/org/org-role-fns"; + +import { TRoleScopeFactory } from "../role-types"; + +type TOrgRoleScopeFactoryDep = { + permissionService: Pick; + externalGroupOrgRoleMappingDAL: Pick; +}; + +export const newOrgRoleFactory = ({ + permissionService, + externalGroupOrgRoleMappingDAL +}: TOrgRoleScopeFactoryDep): TRoleScopeFactory => { + const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const isCustomRole: TRoleScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); + + const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role); + }; + + const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role); + }; + + const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); + + const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ + orgId: dto.permission.orgId, + roleId: dto.selector.id + }); + + 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 onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + }; + + const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + }; + + const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); + }; + + const getPredefinedRoles: TRoleScopeFactory["getPredefinedRoles"] = async (scopeData) => { + const scopeField = getScopeField(scopeData); + return [ + { + id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid + name: "Admin", + slug: "admin", + orgId: scopeField.value, + description: "Complete administration access over the organization", + permissions: orgAdminPermissions, + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response + name: "Member", + slug: "member", + orgId: scopeField.value, + description: "Non-administrative role in an organization", + permissions: orgMemberPermissions, + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response + name: "No Access", + slug: "no-access", + orgId: scopeField.value, + description: "No access to any resources in the organization", + permissions: orgNoAccessPermissions, + createdAt: new Date(), + updatedAt: new Date() + } + ]; + }; + + return { + onCreateRoleGuard, + onUpdateRoleGuard, + onDeleteRoleGuard, + onListRoleGuard, + onGetRoleByIdGuard, + onGetRoleBySlugGuard, + getScopeField, + getPredefinedRoles, + isCustomRole + }; +}; diff --git a/backend/src/services/role/project/project-role-factory.ts b/backend/src/services/role/project/project-role-factory.ts new file mode 100644 index 000000000..2ab4e59d0 --- /dev/null +++ b/backend/src/services/role/project/project-role-factory.ts @@ -0,0 +1,203 @@ +import { ForbiddenError } from "@casl/ability"; +import { v4 as uuidv4 } from "uuid"; + +import { AccessScope, ActionProjectType, ProjectMembershipRole, ProjectType } from "@app/db/schemas"; +import { + cryptographicOperatorPermissions, + projectAdminPermissions, + projectMemberPermissions, + projectNoAccessPermissions, + projectViewerPermission, + sshHostBootstrapPermissions +} from "@app/ee/services/permission/default-roles"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + isCustomProjectRole, + ProjectPermissionActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError } from "@app/lib/errors"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TRoleScopeFactory } from "../role-types"; + +type TProjectRoleScopeFactoryDep = { + permissionService: Pick; + projectDAL: Pick; +}; + +export const newProjectRoleFactory = ({ + permissionService, + projectDAL +}: TProjectRoleScopeFactoryDep): TRoleScopeFactory => { + const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const isCustomRole: TRoleScopeFactory["isCustomRole"] = (role: string) => isCustomProjectRole(role); + + const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); + }; + + const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role); + }; + + const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role); + }; + + const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + }; + + const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + }; + + const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role); + }; + + const getPredefinedRoles: TRoleScopeFactory["getPredefinedRoles"] = async (scopeData) => { + const scope = getScopeField(scopeData); + const project = await projectDAL.findById(scope.value); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + return [ + { + id: uuidv4(), + name: "Admin", + slug: ProjectMembershipRole.Admin, + permissions: projectAdminPermissions, + description: "Full administrative access over a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId + }, + { + id: uuidv4(), + name: "Developer", + slug: ProjectMembershipRole.Member, + permissions: projectMemberPermissions, + description: "Limited read/write role in a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId + }, + { + id: uuidv4(), + name: "SSH Host Bootstrapper", + slug: ProjectMembershipRole.SshHostBootstrapper, + permissions: sshHostBootstrapPermissions, + description: "Create and issue SSH Hosts in a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId, + type: ProjectType.SSH + }, + { + id: uuidv4(), + name: "Cryptographic Operator", + slug: ProjectMembershipRole.KmsCryptographicOperator, + permissions: cryptographicOperatorPermissions, + description: "Perform cryptographic operations, such as encryption and signing, in a project", + createdAt: new Date(), + updatedAt: new Date(), + projectId, + type: ProjectType.KMS + }, + { + id: uuidv4(), + name: "Viewer", + slug: ProjectMembershipRole.Viewer, + permissions: projectViewerPermission, + description: "Only read role in a project", + createdAt: new Date(), + projectId, + updatedAt: new Date() + }, + { + id: uuidv4(), + name: "No Access", + slug: ProjectMembershipRole.NoAccess, + permissions: projectNoAccessPermissions, + description: "No access to any resources in the project", + createdAt: new Date(), + projectId, + updatedAt: new Date() + } + ].filter(({ type }) => (type ? type === project.type : true)); + }; + + return { + onCreateRoleGuard, + onUpdateRoleGuard, + onDeleteRoleGuard, + onListRoleGuard, + onGetRoleByIdGuard, + onGetRoleBySlugGuard, + getScopeField, + getPredefinedRoles, + isCustomRole + }; +}; diff --git a/backend/src/services/role/role-dal.ts b/backend/src/services/role/role-dal.ts new file mode 100644 index 000000000..9f3d7cf61 --- /dev/null +++ b/backend/src/services/role/role-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TRoleDALFactory = ReturnType; + +export const roleDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Role); + return orm; +}; diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts new file mode 100644 index 000000000..41c825b2e --- /dev/null +++ b/backend/src/services/role/role-service.ts @@ -0,0 +1,276 @@ +import { packRules } from "@casl/ability/extra"; +import { requestContext } from "@fastify/request-context"; + +import { AccessScope, ActionProjectType, TableName } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { UnpackedPermissionSchema, unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; + +import { ActorType } from "../auth/auth-type"; +import { TExternalGroupOrgRoleMappingDALFactory } from "../external-group-org-role-mapping/external-group-org-role-mapping-dal"; +import { TIdentityDALFactory } from "../identity/identity-dal"; +import { TProjectDALFactory } from "../project/project-dal"; +import { TUserDALFactory } from "../user/user-dal"; +import { newNamespaceRoleFactory } from "./namespace/namespace-role-factory"; +import { newOrgRoleFactory } from "./org/org-role-factory"; +import { newProjectRoleFactory } from "./project/project-role-factory"; +import { TRoleDALFactory } from "./role-dal"; +import { + TCreateRoleDTO, + TDeleteRoleDTO, + TGetRoleByIdDTO, + TGetRoleBySlugDTO, + TGetUserPermissionDTO, + TListRoleDTO, + TUpdateRoleDTO +} from "./role-types"; + +type TRoleServiceFactoryDep = { + roleDAL: TRoleDALFactory; + identityDAL: Pick; + userDAL: Pick; + permissionService: Pick; + projectDAL: Pick; + externalGroupOrgRoleMappingDAL: Pick; +}; + +export type TRoleServiceFactory = ReturnType; + +export const roleServiceFactory = ({ + roleDAL, + permissionService, + projectDAL, + identityDAL, + userDAL, + externalGroupOrgRoleMappingDAL +}: TRoleServiceFactoryDep) => { + const orgRoleFactory = newOrgRoleFactory({ + permissionService, + externalGroupOrgRoleMappingDAL + }); + const projectRoleFactory = newProjectRoleFactory({ + permissionService, + projectDAL + }); + const namespaceRoleFactory = newNamespaceRoleFactory({ + permissionService + }); + const scopeFactory = { + [AccessScope.Organization]: orgRoleFactory, + [AccessScope.Project]: projectRoleFactory, + [AccessScope.Namespace]: namespaceRoleFactory + }; + + const createRole = async (dto: TCreateRoleDTO) => { + const { data, scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + await factory.onCreateRoleGuard(dto); + + const scope = factory.getScopeField(scopeData); + const existingRole = await roleDAL.findOne({ + slug: data.slug, + [scope.key]: scope.value + }); + if (existingRole) throw new NotFoundError({ message: `Role with ${data.slug} exists` }); + + validateHandlebarTemplate("Role Creation", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const role = await roleDAL.create({ + name: data.name, + description: data.description, + slug: data.slug, + permissions: data.permissions, + [scope.key]: scope.value + }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const updateRole = async (dto: TUpdateRoleDTO) => { + const { data, scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + const scope = factory.getScopeField(scopeData); + + await factory.onUpdateRoleGuard(dto); + + const existingRole = await roleDAL.findOne({ + id: dto.selector.id, + [scope.key]: scope.value + }); + if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` }); + + if (data.slug) { + const existingSlug = await roleDAL.findOne({ + slug: data.slug, + [scope.key]: scope.value + }); + if (existingSlug && existingRole.id !== existingSlug.id) + throw new BadRequestError({ message: `Role with ${data.slug} already exists` }); + } + + validateHandlebarTemplate("Role Update", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const role = await roleDAL.updateById(existingRole.id, { + name: data?.name, + description: data?.description, + slug: data?.slug, + permissions: data?.permissions + }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const deleteRole = async (dto: TDeleteRoleDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + const scope = factory.getScopeField(scopeData); + await factory.onDeleteRoleGuard(dto); + + const existingRole = await roleDAL.findOne({ + id: dto.selector.id, + [scope.key]: scope.value + }); + if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` }); + + const [role] = await roleDAL.delete({ + id: existingRole.id, + [scope.key]: scope.value + }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const listRoles = async (dto: TListRoleDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListRoleGuard(dto); + + const scope = factory.getScopeField(scopeData); + const predefinedRoles = await factory.getPredefinedRoles(scopeData); + const roles = await roleDAL.find( + { + [scope.key]: scope.value + }, + { limit: dto.data.limit, offset: dto.data.offset, sort: [[`${TableName.Role}.slug` as "slug", "asc"]] } + ); + + return { + roles: [...predefinedRoles, ...roles.map((el) => ({ ...el, permissions: unpackPermissions(el.permissions) }))] + }; + }; + + const getRoleById = async (dto: TGetRoleByIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + 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 + }); + if (!role) throw new NotFoundError({ message: `Role with id ${dto.selector.id} not found` }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const getRoleBySlug = async (dto: TGetRoleBySlugDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetRoleBySlugGuard(dto); + + const scope = factory.getScopeField(scopeData); + const isCustomRole = factory.isCustomRole(dto.selector.slug); + if (!isCustomRole) { + const predefinedRole = await factory.getPredefinedRoles(scopeData); + const selectedRole = predefinedRole.find((el) => el.slug === dto.selector.slug); + if (!selectedRole) throw new BadRequestError({ message: `Role with slug ${dto.selector.slug} not found` }); + return { ...selectedRole, permissions: UnpackedPermissionSchema.array().parse(selectedRole.permissions) }; + } + + const role = await roleDAL.findOne({ + slug: selector.slug, + [scope.key]: scope.value + }); + if (!role) throw new NotFoundError({ message: `Role with slug ${dto.selector.slug} not found` }); + + return { ...role, [scope.key]: scope.value, permissions: unpackPermissions(role.permissions) }; + }; + + const getUserPermission = async (dto: TGetUserPermissionDTO) => { + if (dto.scopeData.scope === AccessScope.Organization) { + const { permission, memberships } = await permissionService.getOrgPermission( + dto.permission.type, + dto.permission.id, + dto.permission.orgId, + dto.permission.authMethod, + dto.permission.orgId + ); + return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails: undefined }; + } + + if (dto.scopeData.scope === AccessScope.Project) { + const { permission, memberships } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: dto.scopeData.projectId, + actorOrgId: dto.permission.orgId + }); + + const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails"); + const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === dto.scopeData.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 }; + } + + throw new BadRequestError({ message: "Invalid scope defined" }); + }; + + return { + createRole, + updateRole, + deleteRole, + listRoles, + getRoleById, + getRoleBySlug, + getUserPermission + }; +}; diff --git a/backend/src/services/role/role-types.ts b/backend/src/services/role/role-types.ts new file mode 100644 index 000000000..96fc9c0f3 --- /dev/null +++ b/backend/src/services/role/role-types.ts @@ -0,0 +1,79 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; + +import { AccessScopeData, TRoles } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TRoleScopeFactory { + onCreateRoleGuard: (arg: TCreateRoleDTO) => Promise; + onUpdateRoleGuard: (arg: TUpdateRoleDTO) => Promise; + onDeleteRoleGuard: (arg: TDeleteRoleDTO) => Promise; + onListRoleGuard: (arg: TListRoleDTO) => Promise; + getPredefinedRoles: (arg: AccessScopeData) => Promise<(TRoles & { permissions: RawRuleOf[] })[]>; + onGetRoleByIdGuard: (arg: TGetRoleByIdDTO) => Promise; + onGetRoleBySlugGuard: (arg: TGetRoleBySlugDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; + isCustomRole: (role: string) => boolean; +} + +export type TCreateRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + name: string; + description?: string | null; + slug: string; + permissions: unknown; + }; +}; + +export type TUpdateRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + }; + data: Partial<{ + name: string; + description?: string | null; + slug: string; + permissions: unknown; + }>; +}; + +export type TListRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + limit?: number; + offset?: number; + }; +}; + +export type TDeleteRoleDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + }; +}; + +export type TGetRoleByIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + }; +}; + +export type TGetRoleBySlugDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + slug: string; + }; +}; + +export type TGetUserPermissionDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; +}; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 30cd93291..36f3459cf 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -385,7 +385,7 @@ export const secretImportServiceFactory = ({ path: secretPath, id: secretImportDocId }: TResyncSecretImportReplicationDTO) => { - const { permission, membership } = await permissionService.getProjectPermission({ + const { permission, memberships } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -437,7 +437,7 @@ export const secretImportServiceFactory = ({ secretImportDoc.importPath ); - if (membership && sourceFolder) { + if (memberships?.length && sourceFolder) { await secretQueueService.replicateSecrets({ orgId: actorOrgId, secretPath: secretImportDoc.importPath, diff --git a/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts b/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts index ec4a3f807..384de47e8 100644 --- a/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts +++ b/backend/src/services/secret-reminder-recipients/secret-reminder-recipients-dal.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { AccessScope, TableName } from "@app/db/schemas"; import { ormify, selectAllTableCols } from "@app/lib/knex"; export type TSecretReminderRecipientsDALFactory = ReturnType; @@ -14,13 +14,13 @@ export const secretReminderRecipientsDALFactory = (db: TDbClient) => { .where({ secretId }) .leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.Project, `${TableName.SecretReminderRecipients}.projectId`, `${TableName.Project}.id`) - .leftJoin(TableName.OrgMembership, (bd) => { + .leftJoin(TableName.Membership, (bd) => { void bd - .on(`${TableName.OrgMembership}.userId`, "=", `${TableName.SecretReminderRecipients}.userId`) - .andOn(`${TableName.OrgMembership}.orgId`, "=", `${TableName.Project}.orgId`); + .on(`${TableName.Membership}.actorUserId`, "=", `${TableName.SecretReminderRecipients}.userId`) + .andOn(`${TableName.Membership}.scopeOrgId`, "=", `${TableName.Project}.orgId`) + .andOn(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Organization])); }) - - .where(`${TableName.OrgMembership}.isActive`, true) + .where(`${TableName.Membership}.isActive`, true) .select(selectAllTableCols(TableName.SecretReminderRecipients)) .select( db.ref("email").withSchema(TableName.Users).as("email"), diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index de8f930ed..579b19b19 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -2395,8 +2395,8 @@ export const secretV2BridgeServiceFactory = ({ projectId: folder.projectId, secretVersions: secretVersionsFilter, findOpt: { - offset, limit, + offset, sort: [["createdAt", "desc"]] } }); diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 0282fa537..7d25dac86 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -2,7 +2,13 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; +import { + AccessScope, + SecretVersionsV2Schema, + TableName, + TSecretVersionsV2, + TSecretVersionsV2Update +} from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; @@ -191,11 +197,11 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; const query = (tx || db.replicaNode())(TableName.SecretVersionV2) .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) - .leftJoin( - TableName.ProjectMembership, - `${TableName.ProjectMembership}.userId`, - `${TableName.SecretVersionV2}.userActorId` - ) + .leftJoin(TableName.Membership, (qb) => { + void qb + .on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) + .andOn(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project])); + }) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) .leftJoin( @@ -210,19 +216,19 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ) .where((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.where(`${TableName.ProjectMembership}.projectId`, projectId); + void qb.where(`${TableName.Membership}.scopeProjectId`, projectId); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); }) .orWhere((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.whereNull(`${TableName.ProjectMembership}.projectId`); + void qb.whereNull(`${TableName.Membership}.scopeProjectId`); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); }) .select( selectAllTableCols(TableName.SecretVersionV2), db.ref("username").withSchema(TableName.Users).as("userActorName"), db.ref("name").withSchema(TableName.Identity).as("identityActorName"), - db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"), + db.ref("id").withSchema(TableName.Membership).as("membershipId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index ed794a664..61507d127 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -4,6 +4,7 @@ import { AxiosError } from "axios"; import { Knex } from "knex"; import { + AccessScope, ProjectMembershipRole, ProjectType, ProjectUpgradeStatus, @@ -43,6 +44,8 @@ import { TIntegrationAuthServiceFactory } from "../integration-auth/integration- import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectDALFactory } from "../project/project-dal"; import { createProjectKey } from "../project/project-fns"; @@ -50,7 +53,6 @@ import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; -import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TReminderServiceFactory } from "../reminder/reminder-types"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; @@ -92,7 +94,9 @@ type TSecretQueueFactoryDep = { projectDAL: TProjectDALFactory; projectBotDAL: TProjectBotDALFactory; projectKeyDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick; + membershipUserDAL: Pick; + membershipRoleDAL: Pick; smtpService: TSmtpService; secretVersionDAL: TSecretVersionDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory; @@ -110,7 +114,6 @@ type TSecretQueueFactoryDep = { keyStore: Pick; auditLogService: Pick; orgService: Pick; - projectUserMembershipRoleDAL: Pick; resourceMetadataDAL: Pick; folderCommitService: Pick; secretSyncQueue: Pick; @@ -173,14 +176,15 @@ export const secretQueueFactory = ({ keyStore, auditLogService, orgService, - projectUserMembershipRoleDAL, projectKeyDAL, resourceMetadataDAL, secretSyncQueue, folderCommitService, reminderService, eventBusService, - licenseService + licenseService, + membershipUserDAL, + membershipRoleDAL }: TSecretQueueFactoryDep) => { const integrationMeter = opentelemetry.metrics.getMeter("Integrations"); const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", { @@ -1165,17 +1169,16 @@ export const secretQueueFactory = ({ // if project v1 create the project ghost user if (project.version === ProjectVersion.V1) { const ghostUser = await orgService.addGhostUser(project.orgId, tx); - const projectMembership = await projectMembershipDAL.create( + const projectMembership = await membershipUserDAL.create( { - userId: ghostUser.user.id, - projectId: project.id + actorUserId: ghostUser.user.id, + scopeOrgId: project.orgId, + scope: AccessScope.Project, + scopeProjectId: project.id }, tx ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); + await membershipRoleDAL.create({ membershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, tx); const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ publicKey: ghostUser.keys.publicKey, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 3a5ccc667..723cd368c 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1283,7 +1283,8 @@ export const secretServiceFactory = ({ }); const { userPermissions, identityPermissions, groupPermissions } = await permissionService.getProjectPermissions( - dto.projectId + dto.projectId, + dto.actorOrgId ); const attachAllowedActions = ( diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 8897a1b3f..84a53f407 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,7 @@ import { CronJob } from "cron"; import { + AccessScope, IdentityAuthMethod, OrgMembershipRole, OrgMembershipStatus, @@ -29,7 +30,6 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; -import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityTokenAuthDALFactory } from "../identity-token-auth/identity-token-auth-dal"; @@ -37,10 +37,12 @@ import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { RootKeyEncryptionStrategy } from "../kms/kms-types"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TMicrosoftTeamsServiceFactory } from "../microsoft-teams/microsoft-teams-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; -import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { UserAliasType } from "../user-alias/user-alias-types"; @@ -64,16 +66,17 @@ type TSuperAdminServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityTokenAuthDAL: TIdentityTokenAuthDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; - identityOrgMembershipDAL: TIdentityOrgDALFactory; orgDAL: TOrgDALFactory; - orgMembershipDAL: TOrgMembershipDALFactory; serverCfgDAL: TSuperAdminDALFactory; userDAL: TUserDALFactory; + membershipUserDAL: TMembershipUserDALFactory; + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; userAliasDAL: Pick; authService: Pick; kmsService: Pick; kmsRootConfigDAL: TKmsRootConfigDALFactory; - orgService: Pick; + orgService: Pick; keyStore: Pick; licenseService: Pick; microsoftTeamsService: Pick; @@ -127,7 +130,6 @@ export const superAdminServiceFactory = ({ userDAL, identityDAL, orgDAL, - orgMembershipDAL, userAliasDAL, authService, orgService, @@ -137,11 +139,13 @@ export const superAdminServiceFactory = ({ licenseService, identityAccessTokenDAL, identityTokenAuthDAL, - identityOrgMembershipDAL, microsoftTeamsService, invalidateCacheQueue, smtpService, - tokenService + tokenService, + membershipIdentityDAL, + membershipUserDAL, + membershipRoleDAL }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -589,10 +593,17 @@ export const superAdminServiceFactory = ({ const { identity, credentials } = await identityDAL.transaction(async (tx) => { const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); - await identityOrgMembershipDAL.create( + const membership = await membershipIdentityDAL.create( { - identityId: newIdentity.id, - orgId: organization.id, + actorIdentityId: newIdentity.id, + scopeOrgId: organization.id, + scope: AccessScope.Organization + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, role: OrgMembershipRole.Admin }, tx @@ -834,18 +845,26 @@ export const superAdminServiceFactory = ({ }); } - await orgDAL.createMembership( + const membership = await orgDAL.createMembership( { - userId: inviteeUser.id, + actorUserId: inviteeUser.id, + scope: AccessScope.Organization, inviteEmail: inviteeEmail, - orgId: org.id, - role: OrgMembershipRole.Admin, + scopeOrgId: org.id, status: inviteeUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, isActive: true }, tx ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + users.push(inviteeUser); } @@ -914,23 +933,32 @@ export const superAdminServiceFactory = ({ actorType: ActorType ) => { if (actorType === ActorType.USER) { - const orgMembership = await orgMembershipDAL.findById(membershipId); + const orgMembership = await membershipUserDAL.findOne({ + scope: AccessScope.Organization, + id: membershipId, + scopeOrgId: organizationId + }); if (!orgMembership) { throw new NotFoundError({ name: "Organization Membership", message: "Organization membership not found" }); } - if (orgMembership.userId === actorId) { + if (orgMembership.actorUserId === actorId) { throw new BadRequestError({ message: "You cannot remove yourself from the organization from the instance management panel." }); } } - const [organizationMembership] = await orgMembershipDAL.delete({ - orgId: organizationId, + const membershipRole = await membershipRoleDAL.findOne({ membershipId }); + if (!membershipRole) { + throw new NotFoundError({ name: "Membership Role", message: "Membership role not found" }); + } + const [organizationMembership] = await membershipUserDAL.delete({ + scopeOrgId: organizationId, + scope: AccessScope.Organization, id: membershipId }); - return organizationMembership; + return { ...organizationMembership, role: membershipRole.role, orgId: organizationId }; }; const joinOrganization = async (orgId: string, actor: OrgServiceActor) => { @@ -946,25 +974,46 @@ export const superAdminServiceFactory = ({ throw new NotFoundError({ message: `Could not organization with ID "${orgId}"` }); } - const existingOrgMembership = await orgMembershipDAL.findOne({ userId: serverAdmin.id, orgId }); + const existingOrgMembership = await membershipUserDAL.findOne({ + actorUserId: serverAdmin.id, + scopeOrgId: org.id, + scope: AccessScope.Organization + }); if (existingOrgMembership) { throw new BadRequestError({ message: `You are already a part of the organization with ID ${orgId}` }); } - const orgMembership = await orgDAL.createMembership({ - userId: serverAdmin.id, - orgId: org.id, - role: OrgMembershipRole.Admin, - status: OrgMembershipStatus.Accepted, - isActive: true + const orgMembership = await orgDAL.transaction(async (tx) => { + const membership = await orgDAL.createMembership( + { + actorUserId: serverAdmin.id, + scopeOrgId: org.id, + status: OrgMembershipStatus.Accepted, + isActive: true, + scope: AccessScope.Organization + }, + tx + ); + const membershipRole = await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + return { ...membership, role: membershipRole.role, orgId: org.id }; }); return orgMembership; }; const resendOrgInvite = async ({ organizationId, membershipId }: TResendOrgInviteDTO, actor: OrgServiceActor) => { - const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId }); + const orgMembership = await membershipUserDAL.findOne({ + id: membershipId, + scopeOrgId: organizationId, + scope: AccessScope.Organization + }); if (!orgMembership) { throw new NotFoundError({ name: "Organization Membership", message: "Organization membership not found" }); @@ -976,7 +1025,7 @@ export const superAdminServiceFactory = ({ }); } - if (!orgMembership.userId) { + if (!orgMembership.actorUserId) { throw new NotFoundError({ message: "Cannot find user associated with Org Membership." }); } @@ -984,15 +1033,15 @@ export const superAdminServiceFactory = ({ throw new BadRequestError({ message: "No invite email associated with user." }); } - const org = await orgDAL.findOrgById(orgMembership.orgId); + const org = await orgDAL.findOrgById(orgMembership.scopeOrgId); const appCfg = getConfig(); const serverAdmin = await userDAL.findById(actor.id); const token = await tokenService.createTokenForUser({ type: TokenType.TOKEN_EMAIL_ORG_INVITATION, - userId: orgMembership.userId, - orgId: orgMembership.orgId + userId: orgMembership.actorUserId, + orgId: orgMembership.scopeOrgId }); await smtpService.sendMail({ @@ -1004,13 +1053,13 @@ export const superAdminServiceFactory = ({ inviterUsername: serverAdmin?.email, organizationName: org?.name, email: orgMembership.inviteEmail, - organizationId: orgMembership.orgId, + organizationId: orgMembership.scopeOrgId, token, callback_url: `${appCfg.SITE_URL}/signupinvite` } }); - return orgMembership; + return { ...orgMembership, orgId: organizationId, role: "" }; }; const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => { diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 0da8a46a8..41b615b5d 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -2,6 +2,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { + AccessScope, TableName, TUserActionsInsert, TUserActionsUpdate, @@ -127,9 +128,13 @@ export const userDALFactory = (db: TDbClient) => { const findUserByProjectMembershipId = async (projectMembershipId: string) => { try { return await db - .replicaNode()(TableName.ProjectMembership) - .where({ [`${TableName.ProjectMembership}.id` as "id"]: projectMembershipId }) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .where({ + [`${TableName.Membership}.id` as "id"]: projectMembershipId, + [`${TableName.Membership}.scope` as "scope"]: AccessScope.Project + }) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .first(); } catch (error) { throw new DatabaseError({ error, name: "Find user by project membership id" }); @@ -139,9 +144,11 @@ export const userDALFactory = (db: TDbClient) => { const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => { try { return await db - .replicaNode()(TableName.ProjectMembership) - .whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds) - .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .replicaNode()(TableName.Membership) + .whereIn(`${TableName.Membership}.id`, projectMembershipIds) + .where(`${TableName.Membership}.scope`, AccessScope.Project) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .select("*"); } catch (error) { throw new DatabaseError({ error, name: "Find users by project membership ids" }); @@ -191,8 +198,10 @@ export const userDALFactory = (db: TDbClient) => { try { const doc = await db(TableName.Users) .where({ email }) - .leftJoin(TableName.OrgMembership, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) - .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .leftJoin(TableName.Membership, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .select(selectAllTableCols(TableName.Users)) .select( db.ref("name").withSchema(TableName.Organization).as("orgName"), diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index f5210290c..b54eab8ef 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; +import { AccessScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto"; @@ -9,12 +10,11 @@ import { logger } from "@app/lib/logger"; 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 { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { AuthMethod, AuthTokenType } from "../auth/auth-type"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; -import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TUserAliasDALFactory } from "../user-alias/user-alias-dal"; import { TUserDALFactory } from "./user-dal"; import { TListUserGroupsDTO, TUpdateUserEmailDTO, TUpdateUserMfaDTO } from "./user-types"; @@ -37,9 +37,8 @@ type TUserServiceFactoryDep = { >; groupProjectDAL: Pick; orgDAL: Pick; - orgMembershipDAL: Pick; + membershipUserDAL: Pick; tokenService: Pick; - projectMembershipDAL: Pick; smtpService: Pick; permissionService: TPermissionServiceFactory; userAliasDAL: Pick; @@ -50,8 +49,7 @@ export type TUserServiceFactory = ReturnType; export const userServiceFactory = ({ userDAL, orgDAL, - orgMembershipDAL, - projectMembershipDAL, + membershipUserDAL, groupProjectDAL, tokenService, smtpService, @@ -183,13 +181,19 @@ export const userServiceFactory = ({ }; const checkUserScimRestriction = async (userId: string, tx?: Knex) => { - const userOrgs = await orgMembershipDAL.find({ userId }, { tx }); + const userOrgs = await membershipUserDAL.find( + { + actorUserId: userId, + scope: AccessScope.Organization + }, + { tx } + ); if (userOrgs.length === 0) { return false; } - const orgIds = userOrgs.map((membership) => membership.orgId); + const orgIds = userOrgs.map((membership) => membership.scopeOrgId); const organizations = await orgDAL.find({ $in: { id: orgIds } }, { tx }); return organizations.some((org) => org.scimEnabled); @@ -397,9 +401,10 @@ export const userServiceFactory = ({ }; const getUserProjectFavorites = async (userId: string, orgId: string) => { - const orgMembership = await orgMembershipDAL.findOne({ - userId, - orgId + const orgMembership = await membershipUserDAL.findOne({ + scope: AccessScope.Organization, + actorUserId: userId, + scopeOrgId: orgId }); if (!orgMembership) { @@ -412,9 +417,10 @@ export const userServiceFactory = ({ }; const updateUserProjectFavorites = async (userId: string, orgId: string, projectIds: string[]) => { - const orgMembership = await orgMembershipDAL.findOne({ - userId, - orgId + const orgMembership = await membershipUserDAL.findOne({ + scope: AccessScope.Organization, + actorUserId: userId, + scopeOrgId: orgId }); if (!orgMembership) { @@ -423,18 +429,20 @@ export const userServiceFactory = ({ }); } - const matchingUserProjectMemberships = await projectMembershipDAL.find({ - userId, + const matchingUserProjectMemberships = await membershipUserDAL.find({ + scope: AccessScope.Project, + scopeOrgId: orgId, + actorUserId: userId, $in: { - projectId: projectIds + scopeProjectId: projectIds } }); const memberProjectFavorites = matchingUserProjectMemberships.map( - (projectMembership) => projectMembership.projectId + (projectMembership) => projectMembership.scopeProjectId as string ); - const updatedOrgMembership = await orgMembershipDAL.updateById(orgMembership.id, { + const updatedOrgMembership = await membershipUserDAL.updateById(orgMembership.id, { projectFavorites: memberProjectFavorites }); diff --git a/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx b/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx index dbbc4a806..88c748386 100644 --- a/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx +++ b/frontend/src/context/OrgPermissionContext/OrgPermissionContext.tsx @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { unpackRules } from "@casl/ability/extra"; import { useSuspenseQuery } from "@tanstack/react-query"; @@ -17,16 +18,24 @@ export const useOrgPermission = () => { select: (el) => el.organizationId }); - const { data } = useSuspenseQuery({ + const { + data: { permission, memberships = [] } + } = useSuspenseQuery({ queryKey: roleQueryKeys.getUserOrgPermissions({ orgId: organizationId }), queryFn: () => fetchUserOrgPermissions({ orgId: organizationId }), select: (res) => { const rule = unpackRules>>(res.permissions); const ability = createMongoAbility(rule, { conditionsMatcher }); - return { permission: ability, membership: res.membership }; + return { permission: ability, memberships: res.memberships }; }, staleTime: Infinity }); - return data; + const hasOrgRole = useCallback( + (role: string) => + memberships?.some((membership) => membership.roles.some((el) => role === el.role)), + [] + ); + + return { permission, memberships, hasOrgRole }; }; diff --git a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx index 871d5c338..a5dcdfc16 100644 --- a/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx +++ b/frontend/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx @@ -20,7 +20,7 @@ export const useProjectPermission = () => { } const { - data: { permission, membership, assumedPrivilegeDetails } + data: { permission, memberships, assumedPrivilegeDetails } } = useSuspenseQuery({ queryKey: roleQueryKeys.getUserProjectPermissions({ projectId }), queryFn: () => fetchUserProjectPermissions({ projectId }), @@ -31,18 +31,16 @@ export const useProjectPermission = () => { return { permission: ability, assumedPrivilegeDetails: data.assumedPrivilegeDetails, - membership: { - ...data.membership, - roles: data.membership.roles.map(({ role }) => role) - } + memberships: data.memberships }; } }); const hasProjectRole = useCallback( - (role: string) => membership?.roles?.includes(role) || false, + (role: string) => + memberships?.some((membership) => membership.roles.some((el) => role === el.role)), [] ); - return { permission, membership, hasProjectRole, assumedPrivilegeDetails }; + return { permission, memberships, hasProjectRole, assumedPrivilegeDetails }; }; 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 e9caa68d5..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,13 +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, - permissions: permissions.length ? packRules(permissions) : [] - }); + } = await apiRequest.post(`/api/v1/organization/${orgId}/roles`, dto); return role; }, @@ -91,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 fd7b28db9..5bca7638c 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -11,7 +11,7 @@ import { groupBy } from "@app/lib/fn/array"; import { omit } from "@app/lib/fn/object"; import { ActorType } from "../auditLogs/enums"; -import { OrgUser, TProjectMembership } from "../users/types"; +import { TUserMembership } from "../users/types"; import { TGetUserOrgPermissionsDTO, TGetUserProjectPermissionDTO, @@ -78,12 +78,9 @@ export const useGetProjectRoleBySlug = (projectId: string, roleSlug: string) => const getOrgRoles = async (orgId: string) => { const { data } = await apiRequest.get<{ - data: { roles: Array & { permissions: unknown }> }; + data: { roles: Array & { permissions: TPermission[] }> }; }>(`/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,18 +99,18 @@ 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 TPermission[] }; }, enabled: Boolean(orgId && roleId) }); export const fetchUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => { - if (orgId === "") return { permissions: [], membership: null }; + if (orgId === "") return { permissions: [], memberships: [] }; const { data } = await apiRequest.get<{ permissions: PackRule>>[]; - membership: OrgUser; + memberships: Array; }>(`/api/v1/organization/${orgId}/permissions`); return data; @@ -127,7 +124,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = select: (data) => { const rule = unpackRules>>(data.permissions); const ability = createMongoAbility(rule, { conditionsMatcher }); - return { permission: ability, membership: data.membership }; + return { permission: ability, memberships: data.memberships }; } }); @@ -135,7 +132,7 @@ export const fetchUserProjectPermissions = async ({ projectId }: TGetUserProject const { data } = await apiRequest.get<{ data: { permissions: PackRule>>[]; - membership: Omit & { roles: { role: string }[] }; + memberships: Array; assumedPrivilegeDetails?: { actorId: string; actorType: ActorType; @@ -181,11 +178,7 @@ export const useGetUserProjectPermissions = ({ projectId }: TGetUserProjectPermi }; } }); - const membership = { - ...data.membership, - roles: data.membership.roles.map(({ role }) => role) - }; - return { permission: ability, membership }; + return { permission: ability, memberships: data.memberships }; } }); diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 6dc59762d..4f912fad2 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -266,6 +266,5 @@ export type TSecretReferenceTraceNode = { export type SecretAccessListEntry = { allowedActions: ProjectPermissionActions[]; id: string; - membershipId: string; name: string; }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 9c2d00b05..7777712ae 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -8,7 +8,6 @@ import { queryClient as qc } from "@app/hooks/api/reactQuery"; import { APIKeyDataV2 } from "../apiKeys/types"; import { MfaMethod } from "../auth/types"; import { TGroupWithProjectMemberships } from "../groups/types"; -import { projectKeys } from "../projects"; import { setAuthToken } from "../reactQuery"; import { subscriptionQueryKeys } from "../subscriptions/queries"; import { userKeys } from "./query-keys"; @@ -190,20 +189,11 @@ export const useAddUsersToOrg = () => { mutationFn: (dto) => { return apiRequest.post("/api/v1/invite-org/signup", dto); }, - onSuccess: (_, { organizationId, projects }) => { + onSuccess: (_, { organizationId }) => { queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) }); queryClient.invalidateQueries({ queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) }); - - projects?.forEach((project) => { - if (project.slug) { - queryClient.invalidateQueries({ - queryKey: projectKeys.getProjectGroupMemberships(project.slug) - }); - } - queryClient.invalidateQueries({ queryKey: projectKeys.getProjectUsers(project.id) }); - }); } }); }; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index f442b4c1f..d313efd23 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -64,6 +64,7 @@ export type OrgUser = { inviteEmail: string; organization: string; role: "owner" | "admin" | "member" | "no-access" | "custom"; + customRoleSlug?: string; status: "invited" | "accepted" | "verified" | "completed"; deniedPermissions: any[]; roleId: string; @@ -72,6 +73,13 @@ export type OrgUser = { lastLoginTime?: string; }; +export type TUserMembership = { + id: string; + scope: string; + scopeOrgId: string; + actorUserId: string; +}; + export type TProjectMembership = { id: string; createdAt: string; @@ -160,9 +168,6 @@ export type AddUserToOrgDTO = { inviteeEmails: string[]; organizationRoleSlug: string; organizationId: string; - - // We need the slug in order to invalidate the groups query. `slug` is only used for invalidation purposes. - projects?: { id: string; slug?: string; projectRoleSlug: string[] }[]; }; export type CreateAPIKeyRes = { 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..517fc7823 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 && ( { control, name: "metadata" }); - useEffect(() => { const identity = popUp?.identity?.data as { identityId: string; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index c1c9f0b0a..d7a151e7f 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -19,6 +19,7 @@ import { useOrganization } from "@app/context"; import { findOrgMembershipRole } from "@app/helpers/roles"; import { useAddUsersToOrg, + useAddUserToWsNonE2EE, useFetchServerStatus, useGetOrgRoles, useGetUserProjects @@ -76,6 +77,7 @@ export const AddOrgMemberModal = ({ const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); const { data: serverDetails } = useFetchServerStatus(); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); + const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE(); const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({ includeRoles: true }); @@ -140,13 +142,24 @@ export const AddOrgMemberModal = ({ return; } + const usernames = emails.split(",").map((email) => email.trim()); const { data } = await addUsersMutateAsync({ organizationId: currentOrg?.id, - inviteeEmails: emails.split(",").map((email) => email.trim()), - organizationRoleSlug: organizationRole.slug, - projects: selectedProjects.map(({ id }) => ({ id, projectRoleSlug: [projectRoleSlug] })) + inviteeEmails: usernames, + organizationRoleSlug: organizationRole.slug }); + await Promise.allSettled( + selectedProjects.map((el) => + addUserToProject({ + orgId: currentOrg.id, + projectId: el.id, + roleSlugs: [projectRoleSlug], + usernames + }) + ) + ); + setCompleteInviteLinks(data?.completeInviteLinks ?? null); // only show this notification when email is configured. diff --git a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx index 01bb45598..97aaff73d 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx @@ -6,6 +6,7 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, Checkbox, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { useOrgPermission } from "@app/context"; +import { OrgMembershipRole } from "@app/helpers/roles"; import { useUpgradePrivilegeSystem } from "@app/hooks/api"; const formSchema = z.object({ @@ -21,7 +22,7 @@ type Props = { }; export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => { - const { membership } = useOrgPermission(); + const { hasOrgRole } = useOrgPermission(); const [step, setStep] = useState<"info" | "upgrade">("info"); const { @@ -65,7 +66,7 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => setStep("info"); }; - const isAdmin = membership?.role === "admin"; + const isAdmin = hasOrgRole(OrgMembershipRole.Admin); return ( @@ -274,7 +275,7 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index b6fc4519b..ad3cb3c6f 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -77,7 +77,8 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) = name: data.identity.name, hasDeleteProtection: data.identity.hasDeleteProtection, role: data.role, - customRole: data.customRole + customRole: data.customRole, + metadata: data.metadata }); }} disabled={!isAllowed} diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx index 8c6d511b1..780a9c5f9 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/ExternalMigrationsTab.tsx @@ -3,13 +3,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button } from "@app/components/v2"; import { useOrgPermission } from "@app/context"; +import { OrgMembershipRole } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { SelectImportFromPlatformModal } from "./components/SelectImportFromPlatformModal"; export const ExternalMigrationsTab = () => { - const { membership } = useOrgPermission(); + const { hasOrgRole } = useOrgPermission(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["selectImportPlatform"] as const); @@ -41,7 +41,7 @@ export const ExternalMigrationsTab = () => { onClick={() => { handlePopUpOpen("selectImportPlatform"); }} - isDisabled={membership?.role !== ProjectMembershipRole.Admin} + isDisabled={!hasOrgRole(OrgMembershipRole.Admin)} leftIcon={} > Import diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx index 2050d53b7..e78ad61b9 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgDeleteSection/OrgDeleteSection.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal } from "@app/components/v2"; import { useOrganization, useOrgPermission } from "@app/context"; +import { OrgMembershipRole } from "@app/helpers/roles"; import { useDeleteOrgById } from "@app/hooks/api"; import { clearSession } from "@app/hooks/api/users/queries"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -11,7 +12,7 @@ export const OrgDeleteSection = () => { const navigate = useNavigate(); const { currentOrg } = useOrganization(); - const { membership } = useOrgPermission(); + const { hasOrgRole } = useOrgPermission(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteOrg"] as const); @@ -52,7 +53,7 @@ export const OrgDeleteSection = () => { variant="outline_bg" type="submit" onClick={() => handlePopUpOpen("deleteOrg")} - isDisabled={Boolean(membership && membership.role !== "admin")} + isDisabled={Boolean(!hasOrgRole(OrgMembershipRole.Admin))} > {`Delete ${currentOrg?.name}`} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx index d48113d4c..bdab03d29 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx @@ -1,16 +1,17 @@ import { useOrgPermission } from "@app/context"; +import { OrgMembershipRole } from "@app/helpers/roles"; import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection"; export const OrgGeneralTab = () => { - const { membership } = useOrgPermission(); + const { hasOrgRole } = useOrgPermission(); return (
- {membership && membership.role === "admin" && } + {hasOrgRole(OrgMembershipRole.Admin) && }
); }; diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index 6d3d7e1ae..97b3c0e3c 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -76,7 +76,9 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => return m.status === "invited" ? "Invited" : "Active"; }; - const roleName = roles?.find((r) => r.slug === membership?.role)?.name; + const roleName = roles?.find( + (r) => r.slug === membership?.role || r.slug === membership?.customRoleSlug + )?.name; return membership ? (
diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx index cdf49cf12..3477440fe 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx @@ -26,6 +26,7 @@ import { } from "@app/context"; import { useAddUsersToOrg, + useAddUserToWsNonE2EE, useGetOrgUsers, useGetProjectRoles, useGetWorkspaceUsers @@ -85,7 +86,8 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { defaultValues: { orgMemberships: [], projectRoleSlugs: [] } }); - const { mutateAsync: addMembersToProject } = useAddUsersToOrg(); + const { mutateAsync: addMemberToOrg } = useAddUsersToOrg(); + const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE(); useEffect(() => { if (requesterEmail) { @@ -119,14 +121,14 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { .map((member) => { if (!member) return null; - if (member.user.email) { - return member.user.email; - } - if (member.user.username) { return member.user.username; } + if (member.user.email) { + return member.user.email; + } + return null; }) .filter(Boolean) as string[]; @@ -139,18 +141,19 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { return; } - if (inviteeEmails.length || newInvitees.length) { - await addMembersToProject({ - inviteeEmails: [...inviteeEmails, ...newInvitees], + if (newInvitees.length) { + await addMemberToOrg({ + inviteeEmails: newInvitees, organizationId: orgId, - organizationRoleSlug: ProjectMembershipRole.Member, // only applies to new invites - projects: [ - { - slug: currentProject.slug, - id: currentProject.id, - projectRoleSlug: projectRoleSlugs.map((role) => role.slug) - } - ] + organizationRoleSlug: ProjectMembershipRole.Member // only applies to new invites + }); + } + if (newInvitees.length || inviteeEmails.length) { + await addUserToProject({ + usernames: [...inviteeEmails, ...newInvitees], + orgId, + projectId: currentProject.id, + roleSlugs: projectRoleSlugs.map((role) => role.slug) }); } } diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx index e34f86892..43467876b 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RoleModal.tsx @@ -90,6 +90,15 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }); handlePopUpToggle("role", false); + if (slug) { + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/roles/$roleSlug` as const, + params: { + roleSlug: slug, + projectId + } + }); + } } else { // create const newRole = await createProjectRole({ diff --git a/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx b/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx index 11f4f0270..74e0eda34 100644 --- a/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx @@ -20,7 +20,7 @@ export const AuditLogsRetentionSection = () => { const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention(); const { currentProject } = useProject(); - const { membership } = useProjectPermission(); + const { hasProjectRole } = useProjectPermission(); const { subscription } = useSubscription(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); @@ -82,7 +82,7 @@ export const AuditLogsRetentionSection = () => { return null; } - const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); return ( <>
diff --git a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx index 4771e0c88..fd0be1d7e 100644 --- a/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx +++ b/frontend/src/pages/project/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -10,7 +10,8 @@ import { ProjectPermissionSub, useOrganization, useProject, - useProjectPermission + useProjectPermission, + useUser } from "@app/context"; import { useToggle } from "@app/hooks"; import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api"; @@ -24,8 +25,9 @@ export const DeleteProjectSection = () => { "leaveWorkspace" ] as const); + const { user } = useUser(); const { currentOrg } = useOrganization(); - const { hasProjectRole, membership } = useProjectPermission(); + const { hasProjectRole } = useProjectPermission(); const { currentProject } = useProject(); const [isDeleting, setIsDeleting] = useToggle(); const [isLeaving, setIsLeaving] = useToggle(); @@ -39,14 +41,14 @@ export const DeleteProjectSection = () => { const isNoAccessMember = hasProjectRole("no-access"); const isOnlyAdminMember = useMemo(() => { - if (!members || !membership || !hasProjectRole("admin")) return false; + if (!members || !hasProjectRole("admin")) return false; const adminMembers = members.filter( - (member) => member.roles.map((r) => r.role).includes("admin") && member.id !== membership.id // exclude the current user + (member) => member.roles.map((r) => r.role).includes("admin") && member.user.id !== user.id // exclude the current user ); return !adminMembers.length; - }, [members, membership]); + }, [members, user]); const handleDeleteWorkspaceSubmit = async () => { setIsDeleting.on(); diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx index 048589827..433147429 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx @@ -38,7 +38,7 @@ export const SecretV2MigrationSection = () => { currentProject?.upgradeStatus === ProjectUpgradeStatus.InProgress ? 2000 : false } ); - const { membership } = useProjectPermission(); + const { hasProjectRole } = useProjectPermission(); const migrateProjectToV3 = useMigrateProjectToV3(); const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) }); useEffect(() => { @@ -81,7 +81,7 @@ export const SecretV2MigrationSection = () => { } }; - const isAdmin = membership?.roles.includes(ProjectMembershipRole.Admin); + const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); return (
{isUpgrading && ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index d423264b5..3fd25e423 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -746,18 +746,9 @@ export const SecretDetailSidebar = ({ .join(", ")} className="z-[100] capitalize" > - +
{user.name} - +
))} diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx index 3e749aa0e..ccd15112f 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/BackfillSecretReferenceSection/BackfillSecretReferenceSection.tsx @@ -6,7 +6,7 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; export const BackfillSecretReferenceSecretion = () => { const { currentProject } = useProject(); - const { membership } = useProjectPermission(); + const { hasProjectRole } = useProjectPermission(); const backfillSecretReferences = useBackfillSecretReference(); if (!currentProject) return null; @@ -21,7 +21,7 @@ export const BackfillSecretReferenceSecretion = () => { } }; - const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); return (
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx index bd8d15887..7a0d2650a 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx @@ -18,7 +18,7 @@ export const PointInTimeVersionLimitSection = () => { const { mutateAsync: updateProject } = useUpdateProject(); const { currentProject, projectId } = useProject(); - const { membership } = useProjectPermission(); + const { hasProjectRole } = useProjectPermission(); const { control, @@ -52,7 +52,7 @@ export const PointInTimeVersionLimitSection = () => { } }; - const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); return (
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx index a7c6bbc36..32bb20429 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretDetectionIgnoreValuesSection/SecretDetectionIgnoreValuesSection.tsx @@ -24,7 +24,7 @@ type TForm = z.infer; export const SecretDetectionIgnoreValuesSection = () => { const { currentProject } = useProject(); - const { membership } = useProjectPermission(); + const { hasProjectRole } = useProjectPermission(); const { mutateAsync: updateProject } = useUpdateProject(); const { @@ -73,7 +73,7 @@ export const SecretDetectionIgnoreValuesSection = () => { } }; - const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + const isAdmin = hasProjectRole(ProjectMembershipRole.Admin); if (!currentProject) return null;