diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f4951d8f8..273e6d982 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -48,6 +48,7 @@ import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service"; import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service"; import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-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"; @@ -182,6 +183,8 @@ declare module "fastify" { type: ActorType; id: string; orgId: string; + parentOrgId: string; + rootOrgId: string; }; rateLimits: RateLimitConfiguration; // passport data @@ -335,6 +338,7 @@ declare module "fastify" { additionalPrivilege: TAdditionalPrivilegeServiceFactory; role: TRoleServiceFactory; convertor: TConvertorServiceFactory; + subOrganization: TSubOrgServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/db/migrations/20251018061215_sub-org.ts b/backend/src/db/migrations/20251018061215_sub-org.ts new file mode 100644 index 000000000..1c1d6df74 --- /dev/null +++ b/backend/src/db/migrations/20251018061215_sub-org.ts @@ -0,0 +1,66 @@ +import { Knex } from "knex"; + +import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists"; + +import { AccessScope, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); + if (!hasParentOrgId) { + await knex.schema.alterTable(TableName.Organization, async (t) => { + // the one just above the chain + t.uuid("parentOrgId"); + t.foreign("parentOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + // this would root organization containing various informations like billing etc + t.uuid("rootOrgId"); + t.foreign("rootOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex); + t.unique(["rootOrgId", "parentOrgId", "slug"]); + }); + + // had to switch to raw for null not distinct + } + + const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); + if (!hasIdentityOrgCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + + await knex.raw( + ` + UPDATE ?? AS identity + SET "orgId" = membership."scopeOrgId" + FROM ?? AS membership + WHERE + membership."actorIdentityId" = identity."id" + AND membership."scope" = ? +`, + [TableName.Identity, TableName.Membership, AccessScope.Organization] + ); + + await knex.schema.alterTable(TableName.Identity, (t) => { + t.uuid("orgId").notNullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId"); + const hasRootOrgId = await knex.schema.hasColumn(TableName.Organization, "rootOrgId"); + if (hasParentOrgId || hasRootOrgId) { + await knex.schema.alterTable(TableName.Organization, (t) => { + if (hasParentOrgId) t.dropColumn("parentOrgId"); + if (hasRootOrgId) t.dropColumn("rootOrgId"); + }); + } + + const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId"); + if (hasIdentityOrgCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.dropColumn("orgId"); + }); + } +} diff --git a/backend/src/db/schemas/identities.ts b/backend/src/db/schemas/identities.ts index a592e2480..06c37ff22 100644 --- a/backend/src/db/schemas/identities.ts +++ b/backend/src/db/schemas/identities.ts @@ -13,7 +13,8 @@ export const IdentitiesSchema = z.object({ authMethod: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - hasDeleteProtection: z.boolean().default(false) + hasDeleteProtection: z.boolean().default(false), + orgId: z.string().uuid() }); export type TIdentities = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 28e9471ad..86bc929b8 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -316,6 +316,12 @@ export enum ActionProjectType { Any = "any" } +export enum OrganizationActionScope { + ChildOrganization = "child-organization-only", + ParentOrganization = "parent-organization-only", + Any = "any" +} + export enum TemporaryPermissionMode { Relative = "relative" } diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index afc9e2b73..a1c01151f 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -38,7 +38,9 @@ export const OrganizationsSchema = z.object({ maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), maxSharedSecretViewLimit: z.number().nullable().optional(), googleSsoAuthEnforced: z.boolean().default(false), - googleSsoAuthLastUsed: z.date().nullable().optional() + googleSsoAuthLastUsed: z.date().nullable().optional(), + parentOrgId: z.string().uuid().nullable().optional(), + rootOrgId: z.string().uuid().nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 333fc7e3a..85507c890 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -24,7 +24,8 @@ export async function seed(knex: Knex): Promise { // @ts-ignore id: seedData1.machineIdentity.id, name: seedData1.machineIdentity.name, - authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + orgId: seedData1.organization.id } ]); const identityUa = await knex(TableName.IdentityUniversalAuth) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 42392ba55..31847b503 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -48,12 +48,14 @@ import { registerSshCertRouter } from "./ssh-certificate-router"; import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router"; import { registerSshHostGroupRouter } from "./ssh-host-group-router"; import { registerSshHostRouter } from "./ssh-host-router"; +import { registerSubOrgRouter } from "./sub-org-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization await server.register(registerOrgRoleRouter, { prefix: "/organization" }); + await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" }); await server.register(registerLicenseRouter, { prefix: "/organizations" }); // depreciated in favour of infisical workspace diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 17923975d..2ccdce93a 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -58,7 +58,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const plan = await server.services.license.getOrgPlan({ actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.orgId, + actorOrgId: req.permission.rootOrgId, actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, refreshCache: req.query.refreshCache diff --git a/backend/src/ee/routes/v1/org-role-router.ts b/backend/src/ee/routes/v1/org-role-router.ts index 5a8f03038..591458bb4 100644 --- a/backend/src/ee/routes/v1/org-role-router.ts +++ b/backend/src/ee/routes/v1/org-role-router.ts @@ -3,12 +3,35 @@ import { z } from "zod"; 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 { OrgPermissionSchema, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { BadRequestError } from "@app/lib/errors"; 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 { AuthMode } from "@app/services/auth/auth-type"; +const INVALID_SUBORG_PERMISSIONS = [ + OrgPermissionSubjects.Sso, + OrgPermissionSubjects.Ldap, + OrgPermissionSubjects.Scim, + OrgPermissionSubjects.GithubOrgSync, + OrgPermissionSubjects.GithubOrgSyncManual, + OrgPermissionSubjects.Billing, + OrgPermissionSubjects.SubOrganization +]; + +const validateSubOrganizationSubjects = (permissions: unknown) => { + const invalidPermissionSubjects = (permissions as { subject: OrgPermissionSubjects }[]) + .filter((el) => INVALID_SUBORG_PERMISSIONS.includes(el.subject)) + .map((el) => el.subject); + if (invalidPermissionSubjects.length) { + const deduplication = Array.from(new Set(invalidPermissionSubjects)); + throw new BadRequestError({ + message: `Suborganization contains invalid permission subjects: ${deduplication.join(",")}` + }); + } +}; + export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", @@ -37,6 +60,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId; + if (isSubOrganization) { + validateSubOrganizationSubjects(req.body.permissions); + } + const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions)); const role = await server.services.role.createRole({ permission: req.permission, @@ -133,6 +161,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId; + if (isSubOrganization && req.body.permissions) { + validateSubOrganizationSubjects(req.body.permissions); + } + const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined; const role = await server.services.role.updateRole({ permission: req.permission, diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts new file mode 100644 index 000000000..200130488 --- /dev/null +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -0,0 +1,163 @@ +import { z } from "zod"; + +import { OrganizationsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; +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 { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({ + id: true, + name: true, + slug: true, + createdAt: true, + updatedAt: true, + parentOrgId: true +}); + +export const registerSubOrgRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Create a sub organization", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + name: slugSchema().describe(SUB_ORGANIZATIONS.CREATE.name) + }), + response: { + 200: z.object({ + organization: sanitizedSubOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.createSubOrg({ + name: req.body.name, + permissionActor: req.permission + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.CREATE_SUB_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "List of sub organizations", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + limit: z.coerce.number().min(1).max(1000).default(25).describe(SUB_ORGANIZATIONS.LIST.limit), + offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset), + isAccessible: z + .enum(["true", "false"]) + .optional() + .transform((value) => value === "true") + .describe(SUB_ORGANIZATIONS.LIST.isAccessible) + }), + response: { + 200: z.object({ + organizations: sanitizedSubOrganizationSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organizations } = await server.services.subOrganization.listSubOrgs({ + permissionActor: req.permission, + data: { + limit: req.query.limit, + offset: req.query.offset, + isAccessible: req.query.isAccessible + } + }); + + return { organizations }; + } + }); + + server.route({ + method: "PATCH", + url: "/:subOrgId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Update a sub organization", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.UPDATE.subOrgId) + }), + body: z.object({ + name: slugSchema().describe(SUB_ORGANIZATIONS.UPDATE.name) + }), + response: { + 200: z.object({ + organization: sanitizedSubOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.updateSubOrg({ + subOrgId: req.params.subOrgId, + name: req.body.name, + permissionActor: req.permission + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_SUB_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); +}; diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 5dd0fd4ba..b3cd34ac9 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { AxiosError } from "axios"; -import { TAuditLogs } from "@app/db/schemas"; +import { OrganizationActionScope, TAuditLogs } from "@app/db/schemas"; import { decryptLogStream, decryptLogStreamCredentials, @@ -45,13 +45,14 @@ export const auditLogStreamServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -94,13 +95,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - logStream.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -160,13 +162,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - logStream.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); @@ -185,14 +188,14 @@ export const auditLogStreamServiceFactory = ({ const logStream = await auditLogStreamDAL.findById(logStreamId); if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` }); - - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - logStream.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -206,13 +209,14 @@ export const auditLogStreamServiceFactory = ({ }; const list = async (actor: OrgServiceActor) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index ece5edaf9..eab7792f8 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { requestContext } from "@fastify/request-context"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; @@ -47,13 +47,14 @@ export const auditLogServiceFactory = ({ ); } else { // Organization-wide logs - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAuditLogsActions.Read, diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index f3c95e434..7c73e590b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,6 +173,9 @@ export enum EventType { UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", + CREATE_SUB_ORGANIZATION = "create-sub-organization", + UPDATE_SUB_ORGANIZATION = "update-sub-organization", + ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", @@ -616,6 +619,22 @@ interface GetSecretsEvent { }; } +interface CreateSubOrganizationEvent { + type: EventType.CREATE_SUB_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + +interface UpdateSubOrganizationEvent { + type: EventType.UPDATE_SUB_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + type TSecretMetadata = { key: string; value: string }[]; interface GetSecretEvent { @@ -3964,6 +3983,8 @@ interface PamResourceDeleteEvent { } export type Event = + | CreateSubOrganizationEvent + | UpdateSubOrganizationEvent | GetSecretsEvent | GetSecretEvent | CreateSecretEvent diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 659e07bca..d2d683a84 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { @@ -134,13 +134,14 @@ export const dynamicSecretServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - gateway?.orgId ?? gatewayv2?.orgId, + orgId: gateway?.orgId || gatewayv2?.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, @@ -297,13 +298,14 @@ export const dynamicSecretServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: gateway?.orgId || gatewayv2?.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, diff --git a/backend/src/ee/services/external-kms/external-kms-service.ts b/backend/src/ee/services/external-kms/external-kms-service.ts index d515c5973..9614f3298 100644 --- a/backend/src/ee/services/external-kms/external-kms-service.ts +++ b/backend/src/ee/services/external-kms/external-kms-service.ts @@ -3,6 +3,7 @@ import { STSServiceException } from "@aws-sdk/client-sts"; import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; +import { OrganizationActionScope } from "@app/db/schemas"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; @@ -51,13 +52,14 @@ export const externalKmsServiceFactory = ({ actorOrgId, actorAuthMethod }: TCreateExternalKmsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); const plan = await licenseService.getPlan(actorOrgId); @@ -154,13 +156,14 @@ export const externalKmsServiceFactory = ({ actorAuthMethod }: TUpdateExternalKmsDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); const plan = await licenseService.getPlan(kmsDoc.orgId); @@ -257,13 +260,14 @@ export const externalKmsServiceFactory = ({ const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); @@ -278,13 +282,14 @@ export const externalKmsServiceFactory = ({ }; const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); @@ -294,13 +299,14 @@ export const externalKmsServiceFactory = ({ const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => { const kmsDoc = await kmsDAL.findById(kmsId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); @@ -342,13 +348,14 @@ export const externalKmsServiceFactory = ({ name: kmsName }: TGetExternalKmsBySlugDTO) => { const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - kmsDoc.orgId, + orgId: kmsDoc.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index a2d323790..fd4954a00 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -3,7 +3,7 @@ import net from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { DatabaseErrorCode } from "@app/lib/error-codes"; @@ -73,13 +73,14 @@ export const gatewayV2ServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, actorId, orgId, actorAuthMethod, - orgId - ); + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -258,13 +259,14 @@ export const gatewayV2ServiceFactory = ({ }; const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, @@ -815,13 +817,14 @@ export const gatewayV2ServiceFactory = ({ throw new NotFoundError({ message: `Gateway ${id} not found` }); } - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - gateway.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: gateway.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.DeleteGateways, @@ -845,13 +848,14 @@ export const gatewayV2ServiceFactory = ({ }; const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index 5c8ad80bf..261640cc9 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import { z } from "zod"; +import { OrganizationActionScope } from "@app/db/schemas"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -68,13 +69,14 @@ export const gatewayServiceFactory = ({ "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, actorId, orgId, actorAuthMethod, - orgId - ); + actorOrgId: orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway @@ -480,13 +482,14 @@ export const gatewayServiceFactory = ({ }; const listGateways = async ({ orgPermission }: TListGatewaysDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway @@ -501,13 +504,14 @@ export const gatewayServiceFactory = ({ }; const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway @@ -521,13 +525,14 @@ export const gatewayServiceFactory = ({ }; const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway @@ -542,13 +547,14 @@ export const gatewayServiceFactory = ({ }; const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway 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 b2bcb4ef3..d2713f269 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,7 +6,7 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql"; import { Octokit as OctokitRest } from "@octokit/rest"; import RE2 from "re2"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; @@ -104,13 +104,14 @@ export const githubOrgSyncServiceFactory = ({ githubOrgAccessToken, isActive }: TCreateGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync); const plan = await licenseService.getPlan(orgPermission.orgId); @@ -162,13 +163,14 @@ export const githubOrgSyncServiceFactory = ({ githubOrgAccessToken, isActive }: TUpdateGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + scope: OrganizationActionScope.ParentOrganization, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync); const plan = await licenseService.getPlan(orgPermission.orgId); @@ -226,13 +228,14 @@ export const githubOrgSyncServiceFactory = ({ }; const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: orgPermission.type, + actorId: orgPermission.id, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync); @@ -256,13 +259,14 @@ export const githubOrgSyncServiceFactory = ({ }; const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: orgPermission.id, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); @@ -422,13 +426,14 @@ export const githubOrgSyncServiceFactory = ({ }; const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: orgPermission.id, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); @@ -509,13 +514,14 @@ export const githubOrgSyncServiceFactory = ({ }; const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise => { - const { permission } = await permissionService.getOrgPermission( - orgPermission.type, - orgPermission.id, - orgPermission.orgId, - orgPermission.authMethod, - orgPermission.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: orgPermission.type, + orgId: orgPermission.orgId, + actorId: orgPermission.id, + actorAuthMethod: orgPermission.authMethod, + actorOrgId: orgPermission.orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Edit, diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 075488488..956d7853a 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; -import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, 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"; @@ -73,13 +73,14 @@ export const groupServiceFactory = ({ 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 } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -167,13 +168,14 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); @@ -270,13 +272,14 @@ export const groupServiceFactory = ({ const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -297,17 +300,18 @@ export const groupServiceFactory = ({ const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findById(id); - if (!group) { + if (!group || group.orgId !== actorOrgId) { throw new NotFoundError({ message: `Cannot find group with ID ${id}` }); @@ -330,13 +334,14 @@ export const groupServiceFactory = ({ }: TListGroupUsersDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findOne({ @@ -365,13 +370,14 @@ 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 } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists @@ -451,13 +457,14 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts index ef071742d..10aa3b190 100644 --- a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { @@ -68,13 +69,14 @@ export const identityAuthTemplateServiceFactory = ({ templateFields: Record; } & Omit) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -113,13 +115,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -227,13 +230,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -282,13 +286,14 @@ export const identityAuthTemplateServiceFactory = ({ throw new NotFoundError({ message: "Template not found" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - template.orgId, + orgId: template.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -316,13 +321,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TListIdentityAuthTemplatesDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -352,13 +358,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TGetTemplatesByAuthMethodDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -385,13 +392,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TFindTemplateUsagesDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate @@ -415,13 +423,14 @@ export const identityAuthTemplateServiceFactory = ({ actorOrgId }: TUnlinkTemplateUsageDTO) => { await $checkPlan(actorOrgId); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 27f59a99f..b3eace6bc 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -46,13 +47,14 @@ export const kmipOperationServiceFactory = ({ actorAuthMethod, actorOrgId }: TKmipCreateDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -78,13 +80,14 @@ export const kmipOperationServiceFactory = ({ }; const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -131,13 +134,14 @@ export const kmipOperationServiceFactory = ({ }; const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -189,13 +193,14 @@ export const kmipOperationServiceFactory = ({ }; const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -226,13 +231,14 @@ export const kmipOperationServiceFactory = ({ }; const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -287,13 +293,14 @@ export const kmipOperationServiceFactory = ({ actorAuthMethod, actorOrgId }: TKmipGetAttributesDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -339,13 +346,14 @@ export const kmipOperationServiceFactory = ({ }; const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); @@ -377,13 +385,14 @@ export const kmipOperationServiceFactory = ({ actorOrgId, kmipMetadata }: TKmipRegisterDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 8daa5a37a..482eb41be 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { isValidIp } from "@app/lib/ip"; @@ -401,13 +401,14 @@ export const kmipServiceFactory = ({ }; const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); const kmipConfig = await kmipOrgConfigDAL.findOne({ @@ -566,7 +567,14 @@ export const kmipServiceFactory = ({ }; const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { - await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); const kmipConfig = await kmipOrgConfigDAL.findOne({ orgId: actorOrgId @@ -759,13 +767,14 @@ export const kmipServiceFactory = ({ keyAlgorithm, hostnamesOrIps }: TRegisterServerDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 43ca5ab3d..86bfcc687 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,14 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas"; +import { + AccessScope, + OrganizationActionScope, + 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"; @@ -119,7 +126,14 @@ export const ldapConfigServiceFactory = ({ groupSearchFilter, caCert }: TCreateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -238,7 +252,14 @@ export const ldapConfigServiceFactory = ({ groupSearchFilter, caCert }: TUpdateLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -316,7 +337,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetLdapCfgDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); return getLdapCfg({ orgId @@ -649,7 +677,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetLdapGroupMapsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); const ldapConfig = await ldapConfigDAL.findOne({ @@ -678,7 +713,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateLdapGroupMapDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -732,7 +774,14 @@ export const ldapConfigServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteLdapGroupMapDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); @@ -771,7 +820,14 @@ export const ldapConfigServiceFactory = ({ caCert, url }: TTestLdapConnectionDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); const plan = await licenseService.getPlan(orgId); diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index a2bd7ec51..853f3a994 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -10,6 +10,7 @@ export const licenseDALFactory = (db: TDbClient) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { try { const doc = await (tx || db.replicaNode())(TableName.Membership) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .andWhere((bd) => { if (orgId) { @@ -18,6 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) + .whereNull(`${TableName.Organization}.rootOrgId`) .count(); return Number(doc?.[0]?.count ?? 0); } catch (error) { @@ -25,10 +27,31 @@ export const licenseDALFactory = (db: TDbClient) => { } }; + const countOfOrgIdentities = async (orgId: string | null, tx?: Knex) => { + try { + // count org identities + const identityDoc = await (tx || db.replicaNode())(TableName.Identity) + .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`) + .where((bd) => { + if (orgId) { + void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId); + } + }) + .count(); + + const identityCount = Number(identityDoc?.[0].count); + + return identityCount; + } catch (error) { + throw new DatabaseError({ error, name: "Count of Org Users + Identities" }); + } + }; + const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => { try { // count org users const userDoc = await (tx || db.replicaNode())(TableName.Membership) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .whereNotNull(`${TableName.Membership}.actorUserId`) .andWhere((bd) => { @@ -38,17 +61,17 @@ export const licenseDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) + .whereNull(`${TableName.Organization}.rootOrgId`) .count(); const userCount = Number(userDoc?.[0].count); // count org identities - const identityDoc = await (tx || db.replicaNode())(TableName.Membership) - .where({ scope: AccessScope.Organization }) - .whereNotNull(`${TableName.Membership}.actorIdentityId`) + const identityDoc = await (tx || db.replicaNode())(TableName.Identity) + .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`) .where((bd) => { if (orgId) { - void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); + void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId); } }) .count(); @@ -61,5 +84,5 @@ export const licenseDALFactory = (db: TDbClient) => { } }; - return { countOfOrgMembers, countOrgUsersAndIdentities }; + return { countOfOrgMembers, countOrgUsersAndIdentities, countOfOrgIdentities }; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index aba2c5e78..97061e3ca 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ rbac: false, githubOrgSync: false, customRateLimits: false, + subOrganization: false, customAlerts: false, secretAccessInsights: false, auditLogs: false, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index fba9d0cca..5a53f924c 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -9,12 +9,12 @@ import { AxiosError } from "axios"; import { CronJob } from "cron"; import { Knex } from "knex"; +import { OrganizationActionScope } from "@app/db/schemas"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -45,11 +45,10 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; keyStore: Pick; - identityOrgMembershipDAL: TIdentityOrgDALFactory; projectDAL: TProjectDALFactory; }; @@ -66,7 +65,6 @@ export const licenseServiceFactory = ({ permissionService, licenseDAL, keyStore, - identityOrgMembershipDAL, projectDAL }: TLicenseServiceFactoryDep) => { let isValidLicense = false; @@ -199,19 +197,21 @@ export const licenseServiceFactory = ({ return JSON.parse(cachedPlan) as TFeatureSet; } - const org = await orgDAL.findOrgById(orgId); + const org = await orgDAL.findRootOrgDetails(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const rootOrgId = org.id; + const { data: { currentPlan } } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( `/api/license-server/v1/customers/${org.customerId}/cloud-plan` ); - const workspacesUsed = await projectDAL.countOfOrgProjects(orgId); + const workspacesUsed = await projectDAL.countOfOrgProjects(rootOrgId); currentPlan.workspacesUsed = workspacesUsed; - const membersUsed = await licenseDAL.countOfOrgMembers(orgId); + const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId); currentPlan.membersUsed = membersUsed; - const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId); + const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId); currentPlan.identitiesUsed = identityUsed; if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) { @@ -284,19 +284,20 @@ export const licenseServiceFactory = ({ }; const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { - if (instanceType === InstanceType.Cloud) { - const org = await orgDAL.findOrgById(orgId); - if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const org = await orgDAL.findRootOrgDetails(orgId, tx); + if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); - const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); + const rootOrgId = org.id; + if (instanceType === InstanceType.Cloud) { + const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx); + const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx); if (org?.customerId) { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { quantity, quantityIdentities }); } - await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); + await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId)); } else if (instanceType === InstanceType.EnterpriseOnPrem) { const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); @@ -307,7 +308,7 @@ export const licenseServiceFactory = ({ usedIdentitySeats }); } - await refreshPlan(orgId); + await refreshPlan(rootOrgId); }; // below all are api calls @@ -319,7 +320,14 @@ export const licenseServiceFactory = ({ actorAuthMethod, billingCycle }: TOrgPlansTableDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); const { data } = await licenseServerCloudApi.request.get( `/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` @@ -336,7 +344,14 @@ export const licenseServiceFactory = ({ projectId, refreshCache }: TOrgPlanDTO) => { - await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); if (refreshCache) { await refreshPlan(orgId); } @@ -352,13 +367,20 @@ export const licenseServiceFactory = ({ actorAuthMethod, success_url }: TStartOrgTrialDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -384,13 +406,20 @@ export const licenseServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateOrgPortalSession) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: "Organization not found" @@ -433,10 +462,17 @@ export const licenseServiceFactory = ({ }; const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -502,7 +538,7 @@ export const licenseServiceFactory = ({ const getUsageMetrics = async (orgId: string) => { const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ orgDAL.countAllOrgMembers(orgId), - identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), + licenseDAL.countOfOrgIdentities(orgId), projectDAL.countOfOrgProjects(orgId) ]); @@ -516,10 +552,17 @@ export const licenseServiceFactory = ({ // returns org current plan feature table const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -553,10 +596,17 @@ export const licenseServiceFactory = ({ }; const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -578,13 +628,20 @@ export const licenseServiceFactory = ({ name, email }: TUpdateOrgBillingDetailsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -601,10 +658,17 @@ export const licenseServiceFactory = ({ }; const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -628,13 +692,20 @@ export const licenseServiceFactory = ({ success_url, cancel_url }: TAddOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -660,13 +731,20 @@ export const licenseServiceFactory = ({ orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -692,10 +770,17 @@ export const licenseServiceFactory = ({ }; const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -710,13 +795,20 @@ export const licenseServiceFactory = ({ }; const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -734,13 +826,20 @@ export const licenseServiceFactory = ({ }; const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing ); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -754,10 +853,17 @@ export const licenseServiceFactory = ({ }; const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -771,10 +877,17 @@ export const licenseServiceFactory = ({ }; const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actorId, + actor, + orgId, + actorOrgId, + actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing); - const organization = await orgDAL.findOrgById(orgId); + const organization = await orgDAL.findById(orgId); if (!organization) { throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` @@ -819,7 +932,6 @@ export const licenseServiceFactory = ({ getLicenseId, invalidateGetPlan, updateSubscriptionOrgMemberCount, - refreshPlan, getOrgPlan, getOrgPlansTableByBillCycle, startOrgTrial, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 2276dcf36..93f40ae6e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -33,6 +33,7 @@ export type TFeatureSet = { membersUsed: number; identityLimit: null; identitiesUsed: number; + subOrganization: false; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index c2672a94e..e80ec7cf5 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 { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, 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"; @@ -118,13 +118,14 @@ export const oidcConfigServiceFactory = ({ } if (dto.type === "external") { - const { permission } = await permissionService.getOrgPermission( - dto.actor, - dto.actorId, - dto.organizationId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: dto.actorId, + actor: dto.actor, + orgId: dto.organizationId, + actorOrgId: dto.actorOrgId, + actorAuthMethod: dto.actorAuthMethod, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } @@ -508,13 +509,14 @@ export const oidcConfigServiceFactory = ({ "Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const { permission } = await permissionService.getOrgPermission( - actor, + const { permission } = await permissionService.getOrgPermission({ actorId, - org.id, + actor, + orgId: org.id, + actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); if (org.googleSsoAuthEnforced && isActive) { @@ -602,13 +604,14 @@ export const oidcConfigServiceFactory = ({ "Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); - const { permission } = await permissionService.getOrgPermission( - actor, + const { permission } = await permissionService.getOrgPermission({ actorId, - org.id, + actor, + orgId: org.id, + actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); if (org.googleSsoAuthEnforced && isActive) { @@ -764,7 +767,14 @@ export const oidcConfigServiceFactory = ({ }; const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => { - await permissionService.getOrgPermission(ActorType.USER, actor.id, orgId, actor.authMethod, actor.orgId); + await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: actor.id, + orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); const oidcConfig = await oidcConfigDAL.findOne({ orgId, diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index e9ea76e8c..b8dad991a 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -459,13 +459,14 @@ export const pamAccountServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts index 713383306..26ff7daa6 100644 --- a/backend/src/ee/services/pam-session/pam-session-service.ts +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; @@ -102,13 +102,14 @@ export const pamSessionServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionGatewayActions.CreateGateways, @@ -142,13 +143,14 @@ export const pamSessionServiceFactory = ({ const project = await projectDAL.findById(session.projectId); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - project.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: project.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); if (actor.type === ActorType.IDENTITY) { ForbiddenError.from(permission).throwUnlessCan( diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index d4da8c98f..743dcd63f 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -15,6 +15,11 @@ export enum OrgPermissionActions { Delete = "delete" } +export enum OrgPermissionSubOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export enum OrgPermissionAppConnectionActions { Read = "read", Create = "create", @@ -117,7 +122,8 @@ export enum OrgPermissionSubjects { Kmip = "kmip", Gateway = "gateway", Relay = "relay", - SecretShare = "secret-share" + SecretShare = "secret-share", + SubOrganization = "sub-organization" } export type AppConnectionSubjectFields = { @@ -128,6 +134,7 @@ export type OrgPermissionSet = | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Project] | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization] | [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] @@ -185,6 +192,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") }), + z.object({ + subject: z.literal(OrgPermissionSubjects.SubOrganization).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSubOrgActions).describe( + "Describe what action an entity can take." + ) + }), z.object({ subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") @@ -308,6 +321,10 @@ const buildAdminPermission = () => { // ws permissions can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); + + can(OrgPermissionSubOrgActions.Create, OrgPermissionSubjects.SubOrganization); + can(OrgPermissionSubOrgActions.DirectAccess, OrgPermissionSubjects.SubOrganization); + // role permission can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 49a375f8f..95480a54a 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships { orgAuthEnforced?: boolean | null; orgGoogleSsoAuthEnforced?: boolean | null; shouldUseNewPrivilegeSystem?: boolean | null; + rootOrgId?: string | null; bypassOrgAuthEnabled?: boolean | null; roles: { id: string; @@ -273,7 +274,8 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), 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("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"), + db.ref("rootOrgId").withSchema(TableName.Organization).as("rootOrgId") ); const data = sqlNestRelationships({ @@ -283,6 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => { MembershipsSchema.extend({ orgAuthEnforced: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), + rootOrgId: z.string().optional().nullable(), orgGoogleSsoAuthEnforced: z.boolean(), bypassOrgAuthEnabled: z.boolean() }).parse(el), diff --git a/backend/src/ee/services/permission/permission-service-types.ts b/backend/src/ee/services/permission/permission-service-types.ts index 1f0e00470..c69564b31 100644 --- a/backend/src/ee/services/permission/permission-service-types.ts +++ b/backend/src/ee/services/permission/permission-service-types.ts @@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability"; import { MongoQuery } from "@ucast/mongo2js"; import { Knex } from "knex"; -import { ActionProjectType, TMemberships } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TMemberships } from "@app/db/schemas"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { OrgPermissionSet } from "./org-permission"; @@ -18,21 +18,6 @@ export type TBuildOrgPermissionDTO = { role: string; }[]; -export type TGetUserProjectPermissionArg = { - userId: string; - projectId: string; - authMethod: ActorAuthMethod; - actionProjectType: ActionProjectType; - userOrgId?: string; -}; - -export type TGetIdentityProjectPermissionArg = { - identityId: string; - projectId: string; - identityOrgId?: string; - actionProjectType: ActionProjectType; -}; - export type TGetServiceTokenProjectPermissionArg = { serviceTokenId: string; projectId: string; @@ -54,17 +39,12 @@ export type TGetOrgPermissionArg = { actorId: string; orgId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId?: string; + actorOrgId: string; + scope: OrganizationActionScope; }; export type TPermissionServiceFactory = { - getOrgPermission: ( - type: ActorType, - id: string, - orgId: string, - authMethod: ActorAuthMethod, - actorOrgId: string | undefined - ) => Promise<{ + getOrgPermission: (arg: TGetOrgPermissionArg) => Promise<{ permission: MongoAbility; memberships: Array< TMemberships & { diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 2d879b4fd..48b78d980 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -7,6 +7,7 @@ import { Knex } from "knex"; import { AccessScope, ActionProjectType, + OrganizationActionScope, OrgMembershipRole, ProjectMembershipRole, ServiceTokenScopes @@ -179,14 +180,15 @@ export const permissionServiceFactory = ({ // return minTtl; // }; - const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( - type, - id, + const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ({ + actor, + actorId, orgId, - authMethod, - actorOrgId - ) => { - if (type !== ActorType.USER && type !== ActorType.IDENTITY) { + actorOrgId, + scope, + actorAuthMethod + }) => { + if (actor !== ActorType.USER && actor !== ActorType.IDENTITY) { throw new BadRequestError({ message: "Invalid actor provided", name: "Get org permission" @@ -202,11 +204,19 @@ export const permissionServiceFactory = ({ scope: AccessScope.Organization, orgId }, - actorId: id, - actorType: type + actorId, + actorType: actor }); if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); + const rootOrgId = permissionData?.[0]?.rootOrgId; + const isChild = Boolean(rootOrgId); + if (scope === OrganizationActionScope.ParentOrganization && isChild) { + throw new ForbiddenRequestError({ message: `Child organization cannot do this operation` }); + } else if (scope === OrganizationActionScope.ChildOrganization && !isChild) { + throw new ForbiddenRequestError({ message: `Parent organization cannot do this operation` }); + } + const permissionFromRoles = permissionData.flatMap((membership) => { const activeRoles = membership?.roles .filter( @@ -227,7 +237,7 @@ export const permissionServiceFactory = ({ permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); validateOrgSSO( - authMethod, + actorAuthMethod, permissionData?.[0].orgAuthEnforced, Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), Boolean(permissionData?.[0].bypassOrgAuthEnabled), diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index f3fe07aa8..5a9f04d8d 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; -import { ProjectType, TProjectTemplates } from "@app/db/schemas"; +import { OrganizationActionScope, ProjectType, TProjectTemplates } from "@app/db/schemas"; 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"; @@ -59,13 +59,14 @@ export const projectTemplateServiceFactory = ({ message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates." }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -97,13 +98,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -125,13 +127,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); @@ -152,13 +155,14 @@ export const projectTemplateServiceFactory = ({ message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates." }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); @@ -213,13 +217,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); if (projectTemplate.type !== ProjectType.SecretManager && environments) @@ -272,13 +277,14 @@ export const projectTemplateServiceFactory = ({ if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - projectTemplate.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: projectTemplate.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index 8d9e952a7..1815344a7 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas"; import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { OrgServiceActor } from "@app/lib/types"; +import { ProjectServiceActor } from "@app/lib/types"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; export type TProjectTemplateEnvironment = Pick; @@ -31,7 +31,7 @@ export enum InfisicalProjectTemplate { export type TProjectTemplateServiceFactory = { listProjectTemplatesByOrg: ( - actor: OrgServiceActor, + actor: ProjectServiceActor, type?: ProjectType ) => Promise< ( @@ -85,7 +85,7 @@ export type TProjectTemplateServiceFactory = { >; createProjectTemplate: ( arg: TCreateProjectTemplateDTO, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -109,7 +109,7 @@ export type TProjectTemplateServiceFactory = { updateProjectTemplateById: ( id: string, { roles, environments, ...params }: TUpdateProjectTemplateDTO, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -132,7 +132,7 @@ export type TProjectTemplateServiceFactory = { }>; deleteProjectTemplateById: ( id: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ environments: TProjectTemplateEnvironment[]; roles: { @@ -155,7 +155,7 @@ export type TProjectTemplateServiceFactory = { }>; findProjectTemplateById: ( id: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ packedRoles: TProjectTemplateRole[]; environments: TProjectTemplateEnvironment[]; @@ -179,7 +179,7 @@ export type TProjectTemplateServiceFactory = { }>; findProjectTemplateByName: ( name: string, - actor: OrgServiceActor + actor: ProjectServiceActor ) => Promise<{ packedRoles: TProjectTemplateRole[]; environments: TProjectTemplateEnvironment[]; diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index d791e9919..b2eb932ed 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -3,7 +3,7 @@ import { isIP } from "node:net"; import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; -import { OrgMembershipRole, TRelays } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -126,8 +126,8 @@ export const relayServiceFactory = ({ // generate instance relay CA const instanceRelayCaSerialNumber = createSerialNumber(); - const instanceRelayCaIssuedAt = new Date(); const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceRelayCaIssuedAt = new Date(); const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey); const instanceRelayCaCert = await x509.X509CertificateGenerator.create({ @@ -972,13 +972,14 @@ export const relayServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityId, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityId, orgId, - actorAuthMethod!, - orgId - ); + actorAuthMethod: actorAuthMethod!, + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionRelayActions.CreateRelays, @@ -1102,13 +1103,14 @@ export const relayServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityId, + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityId, orgId, - actorAuthMethod!, - orgId - ); + actorAuthMethod: actorAuthMethod!, + actorOrgId: orgId + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay @@ -1155,13 +1157,14 @@ export const relayServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string; }) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, - actorAuthMethod, + orgId: actorOrgId, + actorAuthMethod: actorAuthMethod!, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay); @@ -1189,13 +1192,14 @@ export const relayServiceFactory = ({ actorAuthMethod: ActorAuthMethod; actorOrgId: string; }) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay); 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 ab84ebd39..13b862343 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -5,6 +5,7 @@ import RE2 from "re2"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -251,7 +252,14 @@ export const samlConfigServiceFactory = ({ authProvider, enableGroupSync }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); @@ -317,7 +325,14 @@ export const samlConfigServiceFactory = ({ authProvider, enableGroupSync }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); const plan = await licenseService.getPlan(orgId); if (!plan.samlSSO) @@ -393,7 +408,7 @@ export const samlConfigServiceFactory = ({ }); } } else if (dto.type === "orgSlug") { - const org = await orgDAL.findOne({ slug: dto.orgSlug }); + const org = await orgDAL.findOne({ slug: dto.orgSlug, rootOrgId: null }); if (!org) { throw new NotFoundError({ message: `Organization with slug '${dto.orgSlug}' not found` @@ -424,13 +439,14 @@ export const samlConfigServiceFactory = ({ // when dto is type id means it's internally used if (dto.type === "org") { - const { permission } = await permissionService.getOrgPermission( - dto.actor, - dto.actorId, - samlConfig.orgId, - dto.actorAuthMethod, - dto.actorOrgId - ); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor: dto.actor, + actorId: dto.actorId, + orgId: samlConfig.orgId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); } const { decryptor } = await kmsService.createCipherPairWithDataKey({ diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index bdf65b988..983ec4db7 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -37,7 +37,7 @@ export type TGetSamlCfgDTO = actor: ActorType; actorId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId: string | undefined; + actorOrgId: string; } | { type: "orgSlug"; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index a08cd5dbf..8b9256023 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -4,6 +4,7 @@ import { scimPatch } from "scim-patch"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -56,6 +57,7 @@ type TScimServiceFactoryDep = { TOrgDALFactory, | "createMembership" | "findById" + | "find" | "findMembership" | "findMembershipWithScimFilter" | "deleteMembershipById" @@ -125,7 +127,14 @@ export const scimServiceFactory = ({ description, ttlDays }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -160,7 +169,14 @@ export const scimServiceFactory = ({ actorAuthMethod, orgId }) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(orgId); @@ -183,13 +199,14 @@ export const scimServiceFactory = ({ let scimToken = await scimDAL.findById(scimTokenId); if (!scimToken) throw new NotFoundError({ message: `SCIM token with ID '${scimTokenId}' not found` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.ParentOrganization, actor, actorId, - scimToken.orgId, + orgId: scimToken.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); const plan = await licenseService.getPlan(scimToken.orgId); diff --git a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts index 85a3cd5f2..a5fbe37a7 100644 --- a/backend/src/ee/services/secret-scanning/secret-scanning-service.ts +++ b/backend/src/ee/services/secret-scanning/secret-scanning-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { WebhookEventMap } from "@octokit/webhooks-types"; import { ProbotOctokit } from "probot"; +import { OrganizationActionScope } 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 { getConfig } from "@app/lib/config/env"; @@ -49,7 +50,14 @@ export const secretScanningServiceFactory = ({ }: TInstallAppSessionDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const sessionId = crypto.randomBytes(16).toString("hex"); @@ -68,13 +76,14 @@ export const secretScanningServiceFactory = ({ const session = await gitAppInstallSessionDAL.findOne({ sessionId }); if (!session) throw new NotFoundError({ message: "Session was not found" }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - session.orgId, + orgId: session.orgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); const installatedApp = await gitAppOrgDAL.transaction(async (tx) => { await gitAppInstallSessionDAL.deleteById(session.id, tx); @@ -117,7 +126,14 @@ export const secretScanningServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetOrgInstallStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const appInstallation = await gitAppOrgDAL.findOne({ orgId }); @@ -125,7 +141,14 @@ export const secretScanningServiceFactory = ({ }; const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const results = await secretScanningDAL.findByOrgId(orgId, filter); @@ -134,7 +157,14 @@ export const secretScanningServiceFactory = ({ }; const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); @@ -150,7 +180,14 @@ export const secretScanningServiceFactory = ({ riskId, status }: TUpdateRiskStatusDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); const isRiskResolved = Boolean( diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts new file mode 100644 index 000000000..74bf784d5 --- /dev/null +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -0,0 +1,160 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas"; +import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TMembershipDALFactory } from "@app/services/membership/membership-dal"; +import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects, OrgPermissionSubOrgActions } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service-types"; +import { TCreateSubOrgDTO, TListSubOrgDTO, TUpdateSubOrgDTO } from "./sub-org-types"; + +type TSubOrgServiceFactoryDep = { + orgDAL: Pick< + TOrgDALFactory, + "findOne" | "create" | "transaction" | "listSubOrganizations" | "updateById" | "findById" + >; + permissionService: Pick; + licenseService: Pick; + membershipDAL: Pick; + membershipRoleDAL: Pick; +}; + +export type TSubOrgServiceFactory = ReturnType; + +export const subOrgServiceFactory = ({ + orgDAL, + permissionService, + licenseService, + membershipDAL, + membershipRoleDAL +}: TSubOrgServiceFactoryDep) => { + const createSubOrg = async ({ name, permissionActor }: TCreateSubOrgDTO) => { + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: permissionActor.orgId, + actorOrgId: permissionActor.orgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ParentOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionSubOrgActions.Create, + OrgPermissionSubjects.SubOrganization + ); + + const orgLicensePlan = await licenseService.getPlan(permissionActor.rootOrgId); + if (!orgLicensePlan.subOrganization) { + throw new BadRequestError({ + message: "Sub-organization creation failed. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + + const existingSubOrg = await orgDAL.findOne({ + parentOrgId: permissionActor.orgId, + name + }); + if (existingSubOrg) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + + const organization = await orgDAL.transaction(async (tx) => { + const org = await orgDAL.create( + { name, slug: name, rootOrgId: permissionActor.rootOrgId, parentOrgId: permissionActor.orgId }, + tx + ); + const membership = await membershipDAL.create( + { + scope: AccessScope.Organization, + [permissionActor.type === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"]: permissionActor.id, + scopeOrgId: org.id, + status: OrgMembershipStatus.Accepted, + isActive: true + }, + tx + ); + await membershipRoleDAL.create( + { + membershipId: membership.id, + role: OrgMembershipRole.Admin + }, + tx + ); + return org; + }); + + return { + organization + }; + }; + + const listSubOrgs = async ({ permissionActor, data }: TListSubOrgDTO) => { + await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: permissionActor.rootOrgId, + actorOrgId: permissionActor.rootOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.Any + }); + + const organizations = await orgDAL.listSubOrganizations({ + actorId: permissionActor.id, + actorType: permissionActor.type, + orgId: permissionActor.rootOrgId, + isAccessible: data?.isAccessible, + limit: data?.limit, + offset: data?.offset + }); + + return { + organizations + }; + }; + + const updateSubOrg = async ({ subOrgId, name, permissionActor }: TUpdateSubOrgDTO) => { + const subOrg = await orgDAL.findOne({ + rootOrgId: permissionActor.rootOrgId, + id: subOrgId + }); + if (!subOrg) { + throw new BadRequestError({ message: "Sub-organization not found" }); + } + + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: subOrgId, + actorOrgId: subOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const existingSubOrg = await orgDAL.findOne({ + parentOrgId: subOrg.parentOrgId, + slug: name + }); + + if (existingSubOrg && existingSubOrg.id !== subOrgId) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + + const organization = await orgDAL.updateById(subOrgId, { name, slug: name }); + + return { + organization + }; + }; + + return { + createSubOrg, + listSubOrgs, + updateSubOrg + }; +}; diff --git a/backend/src/ee/services/sub-org/sub-org-types.ts b/backend/src/ee/services/sub-org/sub-org-types.ts new file mode 100644 index 000000000..a1af9878e --- /dev/null +++ b/backend/src/ee/services/sub-org/sub-org-types.ts @@ -0,0 +1,22 @@ +import { OrgServiceActor } from "@app/lib/types"; + +export type TCreateSubOrgDTO = { + name: string; + permissionActor: OrgServiceActor; +}; + +export type TListSubOrgDTO = { + permissionActor: OrgServiceActor; + data: Partial<{ + limit?: number; + offset?: number; + search?: string; + isAccessible?: boolean; + }>; +}; + +export type TUpdateSubOrgDTO = { + subOrgId: string; + name: string; + permissionActor: OrgServiceActor; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5a336fa11..dde9b4e35 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -33,6 +33,7 @@ export enum ApiDocsTags { LdapAuth = "LDAP Auth", Groups = "Groups", Organizations = "Organizations", + SubOrganizations = "Sub Organizations", Projects = "Projects", ProjectUsers = "Project Users", ProjectGroups = "Project Groups", @@ -717,6 +718,21 @@ export const ORGANIZATIONS = { } } as const; +export const SUB_ORGANIZATIONS = { + CREATE: { + name: "The name of the sub organization to create." + }, + UPDATE: { + name: "The name of the sub organization to update.", + subOrgId: "The id of the sub organization to update." + }, + LIST: { + limit: "The number of sub organizations to return.", + offset: "The offset to start from. If you enter 10, it will start from the 10th sub organization.", + isAccessible: "Filter to only return sub organizations that the actor has access to." + } +} as const; + export const PROJECTS = { CREATE: { organizationSlug: "The slug of the organization to create the project in.", diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index a7a60349f..5fbe8ca79 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -5,7 +5,7 @@ export type TGenericPermission = { actor: ActorType; actorId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId: string | undefined; + actorOrgId: string; }; /** @@ -78,6 +78,15 @@ export type OrgServiceActor = { id: string; authMethod: ActorAuthMethod; orgId: string; + rootOrgId: string; + parentOrgId: string; +}; + +export type ProjectServiceActor = { + type: ActorType; + id: string; + authMethod: ActorAuthMethod; + orgId: string; }; export enum QueueWorkerProfile { diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 1bff11879..b33f2fbe6 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,6 +8,7 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; +import { slugSchema } from "@app/server/lib/schemas"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -20,6 +21,8 @@ export type TAuthMode = tokenVersionId: string; // the session id of token used user: TUsers; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: AuthMethod; isMfaVerified?: boolean; token: AuthModeJwtTokenPayload; @@ -31,6 +34,8 @@ export type TAuthMode = userId: string; user: TUsers; orgId: string; + rootOrgId: string; + parentOrgId: string; token: string; } | { @@ -39,6 +44,8 @@ export type TAuthMode = actor: ActorType.SERVICE; serviceTokenId: string; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: null; token: string; } @@ -48,6 +55,8 @@ export type TAuthMode = identityId: string; identityName: string; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: null; isInstanceAdmin?: boolean; token: TIdentityAccessTokenJwtPayload; @@ -57,6 +66,8 @@ export type TAuthMode = actor: ActorType.SCIM_CLIENT; scimTokenId: string; orgId: string; + rootOrgId: string; + parentOrgId: string; authMethod: null; }; @@ -136,17 +147,26 @@ export const injectIdentity = fp( if (!authMode) return; + const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; + if (subOrganizationSelector) { + await slugSchema().parseAsync(subOrganizationSelector); + } + switch (authMode) { case AuthMode.JWT: { - const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); + const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } = + await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); requestContext.set("orgId", orgId); + req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, - orgId: orgId as string, + orgId, + rootOrgId, + parentOrgId, authMethod: token.authMethod, isMfaVerified: token.isMfaVerified, token @@ -154,13 +174,19 @@ export const injectIdentity = fp( break; } case AuthMode.IDENTITY_ACCESS_TOKEN: { - const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); + const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken( + token, + subOrganizationSelector, + req.realIp + ); const serverCfg = await getServerCfg(); requestContext.set("orgId", identity.orgId); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, orgId: identity.orgId, + rootOrgId: identity.rootOrgId, + parentOrgId: identity.parentOrgId, identityId: identity.identityId, identityName: identity.name, authMethod: null, @@ -190,8 +216,14 @@ export const injectIdentity = fp( case AuthMode.SERVICE_TOKEN: { const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); requestContext.set("orgId", serviceToken.orgId); + + if (subOrganizationSelector) + throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); + req.auth = { orgId: serviceToken.orgId, + rootOrgId: serviceToken.rootOrgId, + parentOrgId: serviceToken.parentOrgId, authMode: AuthMode.SERVICE_TOKEN as const, serviceToken, serviceTokenId: serviceToken.id, @@ -202,22 +234,27 @@ export const injectIdentity = fp( break; } case AuthMode.API_KEY: { - const user = await server.services.apiKey.fnValidateApiKey(token as string); - req.auth = { - authMode: AuthMode.API_KEY as const, - userId: user.id, - actor, - user, - orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon! - authMethod: null, - token: token as string - }; - break; + throw new BadRequestError({ + message: "API key authentication is not supported anymore. Please switch to identity authentication." + }); } case AuthMode.SCIM_TOKEN: { const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); requestContext.set("orgId", orgId); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; + + if (subOrganizationSelector) + throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` }); + + req.auth = { + authMode: AuthMode.SCIM_TOKEN, + actor, + scimTokenId, + orgId, + authMethod: null, + // scim cannot be done for sub organization + rootOrgId: orgId, + parentOrgId: orgId + }; break; } default: diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 11a94657b..827a055d3 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -14,7 +14,9 @@ export const injectPermission = fp(async (server) => { type: ActorType.USER, id: req.auth.userId, orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" - authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + authMethod: req.auth.authMethod, // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId }; logger.info( @@ -25,7 +27,9 @@ export const injectPermission = fp(async (server) => { type: ActorType.IDENTITY, id: req.auth.identityId, orgId: req.auth.orgId, - authMethod: null + authMethod: null, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId }; logger.info( @@ -36,6 +40,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: req.auth.orgId, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId, authMethod: null }; @@ -47,6 +53,8 @@ export const injectPermission = fp(async (server) => { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId, + rootOrgId: req.auth.rootOrgId, + parentOrgId: req.auth.parentOrgId, authMethod: null }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index a805fa1be..53cf1f961 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -131,6 +131,7 @@ import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login- import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal"; import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal"; import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service"; +import { subOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service"; import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; @@ -568,11 +569,10 @@ export const registerRoutes = async ( orgDAL, licenseDAL, keyStore, - identityOrgMembershipDAL, projectDAL }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL }); const membershipUserService = membershipUserServiceFactory({ licenseService, @@ -592,6 +592,7 @@ export const registerRoutes = async ( }); const membershipIdentityService = membershipIdentityServiceFactory({ + identityDAL, membershipIdentityDAL, membershipRoleDAL, orgDAL, @@ -912,6 +913,15 @@ export const registerRoutes = async ( userGroupMembershipDAL, additionalPrivilegeDAL }); + + const subOrgService = subOrgServiceFactory({ + licenseService, + membershipDAL, + membershipRoleDAL, + orgDAL, + permissionService + }); + const signupService = authSignupServiceFactory({ tokenService, smtpService, @@ -1594,10 +1604,12 @@ export const registerRoutes = async ( permissionService, projectDAL, accessTokenQueue, - smtpService + smtpService, + orgDAL }); const identityService = identityServiceFactory({ + additionalPrivilegeDAL, permissionService, identityDAL, identityOrgMembershipDAL, @@ -1628,10 +1640,12 @@ export const registerRoutes = async ( identityAccessTokenDAL, accessTokenQueue, identityDAL, - membershipIdentityDAL + membershipIdentityDAL, + orgDAL }); const identityTokenAuthService = identityTokenAuthServiceFactory({ + identityDAL, identityTokenAuthDAL, identityAccessTokenDAL, permissionService, @@ -1641,6 +1655,7 @@ export const registerRoutes = async ( }); const identityUaService = identityUaServiceFactory({ + identityDAL, permissionService, identityAccessTokenDAL, identityUaClientSecretDAL, @@ -1652,6 +1667,7 @@ export const registerRoutes = async ( }); const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ + identityDAL, identityKubernetesAuthDAL, identityAccessTokenDAL, permissionService, @@ -1665,6 +1681,7 @@ export const registerRoutes = async ( membershipIdentityDAL }); const identityGcpAuthService = identityGcpAuthServiceFactory({ + identityDAL, identityGcpAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1674,6 +1691,7 @@ export const registerRoutes = async ( }); const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityAliCloudAuthDAL, @@ -1683,6 +1701,7 @@ export const registerRoutes = async ( }); const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, identityTlsCertAuthDAL, licenseService, @@ -1692,6 +1711,7 @@ export const registerRoutes = async ( }); const identityAwsAuthService = identityAwsAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityAwsAuthDAL, @@ -1701,6 +1721,7 @@ export const registerRoutes = async ( }); const identityAzureAuthService = identityAzureAuthServiceFactory({ + identityDAL, identityAzureAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1710,6 +1731,7 @@ export const registerRoutes = async ( }); const identityOciAuthService = identityOciAuthServiceFactory({ + identityDAL, identityAccessTokenDAL, orgDAL, identityOciAuthDAL, @@ -1733,6 +1755,7 @@ export const registerRoutes = async ( }); const identityOidcAuthService = identityOidcAuthServiceFactory({ + identityDAL, identityOidcAuthDAL, orgDAL, identityAccessTokenDAL, @@ -1743,6 +1766,7 @@ export const registerRoutes = async ( }); const identityJwtAuthService = identityJwtAuthServiceFactory({ + identityDAL, identityJwtAuthDAL, orgDAL, permissionService, @@ -2296,6 +2320,7 @@ export const registerRoutes = async ( groupProject: groupProjectService, permission: permissionService, org: orgService, + subOrganization: subOrgService, oidc: oidcService, apiKey: apiKeyService, authToken: tokenService, diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 911979b60..48939844e 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { decodedToken.userId, decodedToken.organizationId, decodedToken.authMethod, + decodedToken.organizationId, decodedToken.organizationId ); if (org && org.userTokenExpiration) { 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 3645a8bb6..8f64d3b23 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -73,12 +73,12 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi } }, handler: async (req) => { - const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAliCloudAuth, accessToken, identityAccessToken, identity } = await server.services.identityAliCloudAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 59526899c..3cfb19895 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 @@ -40,12 +40,12 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityAwsAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAwsAuth, accessToken, identityAccessToken, identity } = await server.services.identityAwsAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 2649655bd..cdab7af02 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -35,12 +35,12 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityAzureAuth, accessToken, identityAccessToken, identity } = await server.services.identityAzureAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 d65c46613..474999b2b 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -35,12 +35,12 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityGcpAuth, accessToken, identityAccessToken, identity } = await server.services.identityGcpAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 2a882471d..5d71b3781 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -111,7 +111,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityJwtAuth, accessToken, identityAccessToken, identity } = await server.services.identityJwtAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 0794cf00d..28f611aba 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -56,7 +56,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide } }, handler: async (req) => { - const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityKubernetesAuth, accessToken, identityAccessToken, identity } = await server.services.identityKubernetesAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 caf5708e3..dade20ea3 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -162,13 +162,13 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) const { identityId, user } = req.passportMachineIdentity; - const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({ + const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({ identityId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 24d414286..003d9810b 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -52,12 +52,12 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityOciAuth, accessToken, identityAccessToken, identity } = await server.services.identityOciAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 48fa64bf4..6fad1f400 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -59,7 +59,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) } }, handler: async (req) => { - const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } = + const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } = await server.services.identityOidcAuth.login({ identityId: req.body.identityId, jwt: req.body.jwt @@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_OIDC_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/identity-org-membership-router.ts b/backend/src/server/routes/v1/identity-org-membership-router.ts new file mode 100644 index 000000000..c9b93965a --- /dev/null +++ b/backend/src/server/routes/v1/identity-org-membership-router.ts @@ -0,0 +1,137 @@ +import { z } from "zod"; + +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; +import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs"; +import { ms } from "@app/lib/ms"; +import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const sanitizedOrgIdentityMembershipSchema = z.object({ + id: z.string().uuid(), + orgId: z.string(), + identityId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + // this is hidden so not updating tags + tags: [ApiDocsTags.ProjectIdentities], + description: "Create org identity membership", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + roles: z + .array( + z.union([ + z.object({ + role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + isTemporary: z + .literal(false) + .default(false) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }), + z.object({ + 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(TemporaryPermissionMode) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role) + }) + ]) + ) + .describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description) + .max(1) + }), + response: { + 200: z.object({ + identityMembership: sanitizedOrgIdentityMembershipSchema + }) + } + }, + handler: async (req) => { + const { membership } = await server.services.membershipIdentity.createMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + data: { + identityId: req.params.identityId, + roles: req.body.roles + } + }); + + return { + identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } + }; + } + }); + + server.route({ + method: "DELETE", + url: "/identity-memberships/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + hide: true, + tags: [ApiDocsTags.ProjectIdentities], + description: "Delete org identity memberships", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId) + }), + response: { + 200: z.object({ + identityMembership: sanitizedOrgIdentityMembershipSchema + }) + } + }, + handler: async (req) => { + const { membership } = await server.services.membershipIdentity.deleteMembership({ + permission: req.permission, + scopeData: { + scope: AccessScope.Organization, + orgId: req.permission.orgId + }, + selector: { + identityId: req.params.identityId + } + }); + + return { + identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId } + }; + } + }); +}; diff --git a/backend/src/server/routes/v1/identity-router.ts b/backend/src/server/routes/v1/identity-router.ts index d6a42c4a2..f8e6c78ee 100644 --- a/backend/src/server/routes/v1/identity-router.ts +++ b/backend/src/server/routes/v1/identity-router.ts @@ -249,7 +249,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({ authMethods: z.array(z.string()), activeLockoutAuthMethods: z.array(z.string()) }) @@ -393,7 +393,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({ authMethods: z.array(z.string()) }) }).array(), 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 d549160db..b7a44c62c 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 @@ -64,7 +64,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid throw new BadRequestError({ message: "Missing TLS certificate in header" }); } - const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityTlsCertAuth, accessToken, identityAccessToken, identity } = await server.services.identityTlsCertAuth.login({ identityId: req.body.identityId, clientCertificate: clientCertificate as string @@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, 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 9040d8909..aafffdfdb 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -319,7 +319,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider } }, handler: async (req) => { - const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } = + const { identityTokenAuth, accessToken, identityAccessToken, identity } = await server.services.identityTokenAuth.createTokenAuthToken({ actor: req.permission.type, actorId: req.permission.id, @@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.CREATE_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 0443d35dd..88a4cb775 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -52,14 +52,14 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { accessToken, identityAccessToken, validClientSecretInfo, - identityMembershipOrg, + identity, accessTokenTTL, accessTokenMaxTTL } = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - orgId: identityMembershipOrg.scopeOrgId, + orgId: identity.orgId, event: { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, metadata: { diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 589a79721..4300f5698 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -33,6 +33,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; +import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router"; import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router"; @@ -90,6 +91,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { ); await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); + await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 872b7b157..76b3eae51 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -2,6 +2,7 @@ import RE2 from "re2"; import { z } from "zod"; import { + AccessScope, AuditLogsSchema, GroupsSchema, IncidentContactsSchema, @@ -59,7 +60,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: sanitizedOrganizationSchema + organization: sanitizedOrganizationSchema.extend({ + subOrganization: z + .object({ + id: z.string(), + name: z.string() + }) + .optional() + }) }) } }, @@ -69,6 +77,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { req.permission.id, req.params.organizationId, req.permission.authMethod, + req.permission.rootOrgId, req.permission.orgId ); return { organization }; @@ -467,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { return { groups }; } }); + + server.route({ + method: "GET", + url: "/users/available", + schema: { + response: { + 200: z.object({ + users: z + .object({ + id: z.string().uuid(), + username: z.string(), + email: z.string().nullable().optional(), + firstName: z.string().nullable().optional(), + lastName: z.string().nullable().optional() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { users } = await server.services.membershipUser.listAvailableUsers({ + permission: req.permission, + scopeData: { + orgId: req.permission.orgId, + scope: AccessScope.Organization + }, + data: {} + }); + + return { users }; + } + }); + + server.route({ + method: "GET", + url: "/identities/available", + schema: { + response: { + 200: z.object({ + identities: z + .object({ + id: z.string().uuid(), + name: z.string(), + hasDeleteProtection: z.boolean() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { identities } = await server.services.membershipIdentity.listAvailableIdentities({ + permission: req.permission, + scopeData: { + orgId: req.permission.orgId, + scope: AccessScope.Organization + }, + data: {} + }); + + return { identities }; + } + }); }; diff --git a/backend/src/server/routes/v2/identity-org-router.ts b/backend/src/server/routes/v2/identity-org-router.ts index 8680a2dca..630e09dda 100644 --- a/backend/src/server/routes/v2/identity-org-router.ts +++ b/backend/src/server/routes/v2/identity-org-router.ts @@ -60,7 +60,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { permissions: true, description: true }).optional(), - identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ + identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true }).extend({ authMethods: z.array(z.string()) }) }) diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5e26ebdaf..d599568b3 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType, TAppConnections } from "@app/db/schemas"; +import { ActionProjectType, OrganizationActionScope, TAppConnections } from "@app/db/schemas"; import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; @@ -215,13 +215,14 @@ export const appConnectionServiceFactory = ({ ) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -268,13 +269,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -318,13 +320,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, @@ -342,13 +345,14 @@ export const appConnectionServiceFactory = ({ { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO, actor: OrgServiceActor ) => { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (projectId) { const project = await projectDAL.findProjectById(projectId); @@ -477,13 +481,14 @@ export const appConnectionServiceFactory = ({ "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." ); - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (appConnection.projectId) { const { permission } = await permissionService.getProjectPermission({ @@ -635,13 +640,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Delete, @@ -704,13 +710,14 @@ export const appConnectionServiceFactory = ({ subject(ProjectPermissionSub.AppConnections, { connectionId }) ); } else { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionAppConnectionActions.Connect, @@ -747,13 +754,14 @@ export const appConnectionServiceFactory = ({ }; const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => { - const { permission: orgPermission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission: orgPermission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); let availableProjectConnections: TAppConnections[] = []; @@ -805,13 +813,14 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - appConnection.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: appConnection.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAppConnectionActions.Read, diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 82df0dcb1..28a986fe8 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -3,10 +3,11 @@ import { Knex } from "knex"; 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 { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; + orgDAL: Pick; membershipUserDAL: Pick; }; @@ -80,7 +82,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA }; // to parse jwt identity in inject identity plugin - const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { + const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => { const session = await tokenDAL.findOneTokenSession({ id: token.tokenVersionId, userId: token.userId @@ -207,22 +209,56 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA const user = await userDAL.findById(session.userId); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); + let orgId = ""; + let rootOrgId = ""; + let parentOrgId = ""; if (token.organizationId) { - const orgMembership = await membershipUserDAL.findOne({ - actorUserId: user.id, - scopeOrgId: token.organizationId, - scope: AccessScope.Organization - }); + if (subOrganizationSelector) { + const subOrganization = await orgDAL.findOne({ + rootOrgId: token.organizationId, + slug: subOrganizationSelector + }); + if (!subOrganization) + throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); - if (!orgMembership) { - throw new ForbiddenRequestError({ message: "User not member of organization" }); - } - if (!orgMembership.isActive) { - throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: subOrganization.id, + scope: AccessScope.Organization + }); + + if (!orgMembership) { + throw new ForbiddenRequestError({ message: "User not member of organization" }); + } + + if (!orgMembership.isActive) { + throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + } + orgId = subOrganization.id; + rootOrgId = token.organizationId; + parentOrgId = subOrganization.parentOrgId as string; + } else { + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: token.organizationId, + scope: AccessScope.Organization + }); + + if (!orgMembership) { + throw new ForbiddenRequestError({ message: "User not member of organization" }); + } + + if (!orgMembership.isActive) { + throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); + } + + orgId = token.organizationId; + rootOrgId = token.organizationId; + parentOrgId = token.organizationId; } } - return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId }; + return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId }; }; return { diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index a2e426a2e..14f4387b9 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -258,7 +258,13 @@ export const authSignupServiceFactory = ({ let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; if (organizationId) { - const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId); + const org = await orgService.findOrganizationById( + user.id, + organizationId, + authMethod, + organizationId, + organizationId + ); if (org && org.userTokenExpiration) { tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); refreshTokenExpiresIn = org.userTokenExpiration; 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 a072544ca..e51d25ce3 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 @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; 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"; @@ -26,13 +27,14 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ roleDAL }: TExternalGroupOrgRoleMappingServiceFactoryDep) => { const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); // TODO: will need to change if we add support for ldap, oidc, etc. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim); @@ -48,13 +50,14 @@ export const externalGroupOrgRoleMappingServiceFactory = ({ dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO, actor: OrgServiceActor ) => { - const { permission } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { permission } = await permissionService.getOrgPermission({ + actor: actor.type, + actorId: actor.id, + orgId: actor.orgId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + scope: OrganizationActionScope.ParentOrganization + }); // TODO: will need to change if we add support for ldap, oidc, etc. ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 4192ffcd5..3cc128768 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -1,4 +1,4 @@ -import { OrgMembershipRole } from "@app/db/schemas"; +import { OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { AuditLogInfo, EventType, @@ -89,13 +89,14 @@ export const externalMigrationServiceFactory = ({ throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." }); } - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); } @@ -136,13 +137,14 @@ export const externalMigrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TImportVaultDataDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import data" }); @@ -192,13 +194,14 @@ export const externalMigrationServiceFactory = ({ actorAuthMethod, provider }: THasCustomVaultMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor, + const { hasRole } = await permissionService.getOrgPermission({ actorId, + actor, + orgId: actorOrgId, actorOrgId, actorAuthMethod, - actorOrgId - ); + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); @@ -247,13 +250,14 @@ export const externalMigrationServiceFactory = ({ }; const createVaultExternalMigration = async ({ namespace, connectionId, actor }: TCreateVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can configure vault external migration" }); @@ -298,13 +302,14 @@ export const externalMigrationServiceFactory = ({ connectionId, actor }: TUpdateVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can update vault external migration" }); @@ -332,13 +337,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultExternalMigrationConfigs = async ({ actor }: { actor: OrgServiceActor }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault external migration configs" }); @@ -352,13 +358,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultNamespaces = async ({ actor }: { actor: OrgServiceActor }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault namespaces" }); @@ -380,13 +387,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault policies" }); @@ -422,13 +430,14 @@ export const externalMigrationServiceFactory = ({ }; const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault mounts" }); @@ -472,13 +481,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; mountPath: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault secret paths" }); @@ -531,13 +541,14 @@ export const externalMigrationServiceFactory = ({ vaultSecretPath: string; auditLogInfo: AuditLogInfo; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can import vault secrets" }); @@ -617,13 +628,14 @@ export const externalMigrationServiceFactory = ({ }; const deleteVaultExternalMigration = async ({ id, actor }: TDeleteVaultExternalMigrationDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can delete vault external migration configs" }); @@ -653,13 +665,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; authType?: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault auth mounts" }); @@ -704,13 +717,14 @@ export const externalMigrationServiceFactory = ({ namespace: string; mountPath: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - actor.type, - actor.id, - actor.orgId, - actor.authMethod, - actor.orgId - ); + const { hasRole } = await permissionService.getOrgPermission({ + actorId: actor.id, + actor: actor.type, + orgId: actor.orgId, + actorOrgId: actor.orgId, + actorAuthMethod: actor.authMethod, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ message: "Only admins can view vault Kubernetes auth roles" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 19de362d8..ffdb78645 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -19,6 +19,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select(db.ref("name").withSchema(TableName.Identity)) + .select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId")) .first(); return doc; 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 1f6e4616b..02660a0ae 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 @@ -8,6 +8,7 @@ import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-to import { AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; @@ -19,6 +20,7 @@ type TIdentityAccessTokenServiceFactoryDep = { "updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache" >; membershipIdentityDAL: Pick; + orgDAL: Pick; }; export type TIdentityAccessTokenServiceFactory = ReturnType; @@ -27,7 +29,8 @@ export const identityAccessTokenServiceFactory = ({ identityAccessTokenDAL, accessTokenQueue, identityDAL, - membershipIdentityDAL + membershipIdentityDAL, + orgDAL }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const { @@ -181,7 +184,11 @@ export const identityAccessTokenServiceFactory = ({ return { revokedToken }; }; - const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { + const fnValidateIdentityAccessToken = async ( + token: TIdentityAccessTokenJwtPayload, + subOrganizationSelector?: string, + ipAddress?: string + ) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, isAccessTokenRevoked: false @@ -202,13 +209,40 @@ export const identityAccessTokenServiceFactory = ({ trustedIps: trustedIps as TIp[] }); } - const identityOrgMembership = await membershipIdentityDAL.findOne({ - scope: AccessScope.Organization, - actorIdentityId: identityAccessToken.identityId - }); + let orgId = ""; + let parentOrgId = ""; + const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId }); + const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id; - if (!identityOrgMembership) { - throw new BadRequestError({ message: "Identity does not belong to any organization" }); + if (subOrganizationSelector) { + const subOrganization = await orgDAL.findOne({ rootOrgId, slug: subOrganizationSelector }); + if (!subOrganization) + throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); + + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: subOrganization.id + }); + + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to any organization" }); + } + orgId = subOrganization.id; + parentOrgId = subOrganization.parentOrgId as string; + } else { + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: rootOrgId + }); + + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to any organization" }); + } + + orgId = rootOrgId; + parentOrgId = rootOrgId; } let { accessTokenNumUses } = identityAccessToken; @@ -219,7 +253,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId }; + return { ...identityAccessToken, orgId, rootOrgId, parentOrgId }; }; 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 43584a1af..c6f6f1376 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,11 +13,18 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -34,12 +41,13 @@ import { } from "./identity-alicloud-auth-types"; type TIdentityAliCloudAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityAliCloudAuthDAL: Pick< TIdentityAliCloudAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -48,6 +56,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { export type TIdentityAliCloudAuthServiceFactory = ReturnType; export const identityAliCloudAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityAliCloudAuthDAL, membershipIdentityDAL, @@ -63,12 +72,8 @@ export const identityAliCloudAuthServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAliCloudAuth.identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAliCloudAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const requestUrl = new URL("https://sts.aliyuncs.com"); @@ -93,8 +98,8 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, lastLoginTime: new Date() @@ -135,7 +140,7 @@ export const identityAliCloudAuthServiceFactory = ({ identityAliCloudAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; @@ -162,6 +167,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ @@ -173,13 +181,14 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -238,6 +247,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new NotFoundError({ @@ -255,13 +267,14 @@ export const identityAliCloudAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -304,6 +317,9 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ @@ -313,13 +329,14 @@ export const identityAliCloudAuthServiceFactory = ({ const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -339,27 +356,32 @@ export const identityAliCloudAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { throw new BadRequestError({ message: "The identity does not have Alibaba Cloud auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( 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 8793c3a00..1814afb2e 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,10 +13,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -35,9 +42,10 @@ import { } from "./identity-aws-auth-types"; type TIdentityAwsAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -80,6 +88,7 @@ function isValidAwsRegion(region: string | null): boolean { } export const identityAwsAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityAwsAuthDAL, membershipIdentityDAL, @@ -93,11 +102,8 @@ export const identityAwsAuthServiceFactory = ({ throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAwsAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAwsAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); @@ -159,8 +165,8 @@ export const identityAwsAuthServiceFactory = ({ } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, lastLoginTime: new Date() @@ -212,7 +218,7 @@ export const identityAwsAuthServiceFactory = ({ } ); - return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityAwsAuth, identityAccessToken, identity }; }; const attachAwsAuth = async ({ @@ -240,6 +246,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ @@ -251,13 +260,14 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -320,6 +330,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new NotFoundError({ @@ -336,13 +349,14 @@ export const identityAwsAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -387,6 +401,9 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ @@ -396,13 +413,14 @@ export const identityAwsAuthServiceFactory = ({ const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...awsIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -422,27 +440,32 @@ export const identityAwsAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) { throw new BadRequestError({ message: "The identity does not have aws auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( 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 b3250ed56..f75aeba4f 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -30,11 +37,12 @@ import { } from "./identity-azure-auth-types"; type TIdentityAzureAuthServiceFactoryDep = { + identityDAL: Pick; identityAzureAuthDAL: Pick< TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -44,6 +52,7 @@ type TIdentityAzureAuthServiceFactoryDep = { export type TIdentityAzureAuthServiceFactory = ReturnType; export const identityAzureAuthServiceFactory = ({ + identityDAL, identityAzureAuthDAL, membershipIdentityDAL, identityAccessTokenDAL, @@ -57,11 +66,8 @@ export const identityAzureAuthServiceFactory = ({ throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityAzureAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityAzureAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const azureIdentity = await validateAzureIdentity({ tenantId: identityAzureAuth.tenantId, @@ -86,8 +92,8 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, lastLoginTime: new Date() @@ -125,7 +131,7 @@ export const identityAzureAuthServiceFactory = ({ } ); - return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityAzureAuth, identityAccessToken, identity }; }; const attachAzureAuth = async ({ @@ -153,6 +159,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ @@ -163,13 +172,14 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -232,6 +242,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "Failed to update Azure Auth" @@ -247,13 +260,14 @@ export const identityAzureAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -301,6 +315,9 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have Azure Auth attached" @@ -309,13 +326,14 @@ export const identityAzureAuthServiceFactory = ({ const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityAzureAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -336,27 +354,32 @@ export const identityAzureAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) { throw new BadRequestError({ message: "The identity does not have azure auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, 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 fe7b9b6d7..67adb6c1e 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -31,8 +38,9 @@ import { } from "./identity-gcp-auth-types"; type TIdentityGcpAuthServiceFactoryDep = { + identityDAL: Pick; identityGcpAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -42,6 +50,7 @@ type TIdentityGcpAuthServiceFactoryDep = { export type TIdentityGcpAuthServiceFactory = ReturnType; export const identityGcpAuthServiceFactory = ({ + identityDAL, identityGcpAuthDAL, membershipIdentityDAL, identityAccessTokenDAL, @@ -55,13 +64,8 @@ export const identityGcpAuthServiceFactory = ({ throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityGcpAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); - } + const identity = await identityDAL.findById(identityGcpAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); let gcpIdentityDetails: TGcpIdentityDetails; switch (identityGcpAuth.type) { @@ -125,8 +129,8 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, lastLoginTime: new Date() @@ -164,7 +168,7 @@ export const identityGcpAuthServiceFactory = ({ } ); - return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityGcpAuth, identityAccessToken, identity }; }; const attachGcpAuth = async ({ @@ -193,6 +197,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -204,13 +211,14 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -274,6 +282,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -290,13 +301,14 @@ export const identityGcpAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -345,6 +357,9 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ @@ -354,13 +369,14 @@ export const identityGcpAuthServiceFactory = ({ const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityGcpAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -381,28 +397,33 @@ export const identityGcpAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) { throw new BadRequestError({ message: "The identity does not have gcp auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, 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 a99c8ad78..debd90933 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 { AccessScope, IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope, 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,6 +24,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -43,8 +44,9 @@ import { } from "./identity-jwt-auth-types"; type TIdentityJwtAuthServiceFactoryDep = { + identityDAL: Pick; identityJwtAuthDAL: TIdentityJwtAuthDALFactory; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -55,6 +57,7 @@ type TIdentityJwtAuthServiceFactoryDep = { export type TIdentityJwtAuthServiceFactory = ReturnType; export const identityJwtAuthServiceFactory = ({ + identityDAL, identityJwtAuthDAL, membershipIdentityDAL, permissionService, @@ -69,19 +72,12 @@ export const identityJwtAuthServiceFactory = ({ throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityJwtAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityJwtAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); @@ -211,12 +207,9 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -251,7 +244,7 @@ export const identityJwtAuthServiceFactory = ({ } ); - return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityJwtAuth, identityAccessToken, identity }; }; const attachJwtAuth = async ({ @@ -284,6 +277,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ message: "Failed to add JWT Auth to already configured identity" @@ -294,13 +290,14 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); @@ -387,6 +384,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -403,13 +403,14 @@ export const identityJwtAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); @@ -491,6 +492,9 @@ export const identityJwtAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -498,13 +502,14 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); @@ -539,6 +544,9 @@ export const identityJwtAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.JWT_AUTH)) { throw new BadRequestError({ @@ -546,23 +554,25 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( 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 952b1e31d..49fb597f5 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,12 @@ import axios, { AxiosError } from "axios"; import https from "https"; import RE2 from "re2"; -import { AccessScope, IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; +import { + AccessScope, + IdentityAuthMethod, + OrganizationActionScope, + 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"; @@ -21,13 +26,20 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -48,12 +60,13 @@ import { } from "./identity-kubernetes-auth-types"; type TIdentityKubernetesAuthServiceFactoryDep = { + identityDAL: Pick; identityKubernetesAuthDAL: Pick< TIdentityKubernetesAuthDALFactory, "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -69,6 +82,7 @@ export type TIdentityKubernetesAuthServiceFactory = ReturnType { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -475,7 +479,7 @@ export const identityKubernetesAuthServiceFactory = ({ } ); - return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityKubernetesAuth, identityAccessToken, identity }; }; const attachKubernetesAuth = async ({ @@ -508,6 +512,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ @@ -519,13 +526,14 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -560,13 +568,14 @@ export const identityKubernetesAuthServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway @@ -633,6 +642,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ @@ -650,13 +662,14 @@ export const identityKubernetesAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -692,13 +705,14 @@ export const identityKubernetesAuthServiceFactory = ({ isGatewayV1 = false; } - const { permission: orgPermission } = await permissionService.getOrgPermission( + const { permission: orgPermission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(orgPermission).throwUnlessCan( OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway @@ -779,6 +793,9 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); if (!identityKubernetesAuth) { @@ -791,13 +808,14 @@ export const identityKubernetesAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -841,28 +859,33 @@ export const identityKubernetesAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) { throw new BadRequestError({ message: "The identity does not have kubernetes auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, 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 1a8ea3ed6..272e45c4e 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } 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"; @@ -21,6 +21,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, + ForbiddenRequestError, NotFoundError, PermissionBoundaryError, RateLimitError, @@ -56,11 +57,11 @@ type TIdentityLdapAuthServiceFactoryDep = { TIdentityLdapAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: TKmsServiceFactory; - identityDAL: TIdentityDALFactory; + identityDAL: Pick; identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; keyStore: Pick< TKeyStoreFactory, @@ -150,17 +151,6 @@ export const identityLdapAuthServiceFactory = ({ }; const login = async ({ identityId }: TLoginLdapAuthDTO) => { - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) { - throw new UnauthorizedError({ - message: "Invalid credentials" - }); - } - const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); if (!identityLdapAuth) { @@ -169,7 +159,10 @@ export const identityLdapAuthServiceFactory = ({ }); } - const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); + const identity = await identityDAL.findById(identityLdapAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + + const plan = await licenseService.getPlan(identity.orgId); if (!plan.ldap) { throw new BadRequestError({ message: @@ -178,12 +171,9 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -217,7 +207,7 @@ export const identityLdapAuthServiceFactory = ({ } ); - return { accessToken, identityLdapAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityLdapAuth, identityAccessToken, identity }; }; const attachLdapAuth = async ({ @@ -254,6 +244,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ @@ -265,13 +258,14 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); if (templateId) { @@ -425,6 +419,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new NotFoundError({ @@ -441,13 +438,14 @@ export const identityLdapAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); if (templateId) { @@ -588,6 +586,9 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ @@ -597,13 +598,14 @@ export const identityLdapAuthServiceFactory = ({ const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -635,27 +637,32 @@ export const identityLdapAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) { throw new BadRequestError({ message: "The identity does not have LDAP Auth attached" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -785,13 +792,14 @@ export const identityLdapAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const deleted = await keyStore.deleteItems({ 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 bfac3d158..6d7f0c4d3 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } 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,11 +14,18 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -35,9 +42,10 @@ import { } from "./identity-oci-auth-types"; type TIdentityOciAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityOciAuthDAL: Pick; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; orgDAL: Pick; @@ -46,6 +54,7 @@ type TIdentityOciAuthServiceFactoryDep = { export type TIdentityOciAuthServiceFactory = ReturnType; export const identityOciAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityOciAuthDAL, membershipIdentityDAL, @@ -59,11 +68,8 @@ export const identityOciAuthServiceFactory = ({ throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityOciAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const identity = await identityDAL.findById(identityOciAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { @@ -98,12 +104,9 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -140,7 +143,7 @@ export const identityOciAuthServiceFactory = ({ identityOciAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; @@ -168,6 +171,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ @@ -179,13 +185,14 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -246,6 +253,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new NotFoundError({ @@ -262,13 +272,14 @@ export const identityOciAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -312,6 +323,9 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ @@ -321,13 +335,14 @@ export const identityOciAuthServiceFactory = ({ const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...ociIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -347,27 +362,32 @@ export const identityOciAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) { throw new BadRequestError({ message: "The identity does not have OCI auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(actorOrgId); const permissionBoundary = validatePrivilegeChangeOperation( 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 1218d8e1c..628b69f14 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 { AccessScope, IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope, 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,6 +25,7 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -43,8 +44,9 @@ import { } from "./identity-oidc-auth-types"; type TIdentityOidcAuthServiceFactoryDep = { + identityDAL: Pick; identityOidcAuthDAL: TIdentityOidcAuthDALFactory; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -55,6 +57,7 @@ type TIdentityOidcAuthServiceFactoryDep = { export type TIdentityOidcAuthServiceFactory = ReturnType; export const identityOidcAuthServiceFactory = ({ + identityDAL, identityOidcAuthDAL, membershipIdentityDAL, permissionService, @@ -69,19 +72,12 @@ export const identityOidcAuthServiceFactory = ({ throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityOidcAuth.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityOidcAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityOidcAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); let caCert = ""; @@ -182,12 +178,9 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -226,7 +219,7 @@ export const identityOidcAuthServiceFactory = ({ } ); - return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg, oidcTokenData: tokenData }; + return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData }; }; const attachOidcAuth = async ({ @@ -259,6 +252,9 @@ export const identityOidcAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ message: "Failed to add OIDC Auth to already configured identity" @@ -269,13 +265,14 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); @@ -351,6 +348,9 @@ export const identityOidcAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -367,13 +367,14 @@ export const identityOidcAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); @@ -440,6 +441,9 @@ export const identityOidcAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -447,13 +451,14 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -481,6 +486,9 @@ export const identityOidcAuthServiceFactory = ({ if (!identityMembershipOrg) { throw new NotFoundError({ message: "Failed to find identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) { throw new BadRequestError({ @@ -488,23 +496,25 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 3dba6210d..adcdd8be8 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -25,11 +25,12 @@ import { buildAuthMethods } from "../identity/identity-fns"; export type TIdentityProjectDALFactory = ReturnType; export const identityProjectDALFactory = (db: TDbClient) => { - const findByIdentityId = async (identityId: string, tx?: Knex) => { + const findByIdentityId = async (identityId: string, orgId: string, tx?: Knex) => { try { const docs = await (tx || db.replicaNode())(TableName.Membership) .where(`${TableName.Membership}.actorIdentityId`, identityId) .where(`${TableName.Membership}.scope`, AccessScope.Project) + .where(`${TableName.Membership}.scopeOrgId`, orgId) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.Project, `${TableName.Membership}.scopeProjectId`, `${TableName.Project}.id`) .join(TableName.Identity, `${TableName.Membership}.actorIdentityId`, `${TableName.Identity}.id`) 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 625b9b328..24c82ccac 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -11,10 +11,17 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -25,12 +32,13 @@ import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; type TIdentityTlsCertAuthServiceFactoryDep = { + identityDAL: Pick; identityAccessTokenDAL: Pick; identityTlsCertAuthDAL: Pick< TIdentityTlsCertAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: Pick; @@ -46,6 +54,7 @@ const parseSubjectDetails = (data: string) => { }; export const identityTlsCertAuthServiceFactory = ({ + identityDAL, identityAccessTokenDAL, identityTlsCertAuthDAL, membershipIdentityDAL, @@ -61,20 +70,12 @@ export const identityTlsCertAuthServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityTlsCertAuth.identityId, - scope: AccessScope.Organization - }); - - if (!identityMembershipOrg) { - throw new NotFoundError({ - message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found` - }); - } + const identity = await identityDAL.findById(identityTlsCertAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: identityMembershipOrg.scopeOrgId + orgId: identity.orgId }); const caCertificate = decryptor({ @@ -119,12 +120,9 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -161,7 +159,7 @@ export const identityTlsCertAuthServiceFactory = ({ identityTlsCertAuth, accessToken, identityAccessToken, - identityMembershipOrg + identity }; }; @@ -189,6 +187,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ @@ -200,13 +201,14 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -271,6 +273,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new NotFoundError({ @@ -288,13 +293,14 @@ export const identityTlsCertAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -350,6 +356,9 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ @@ -359,13 +368,14 @@ export const identityTlsCertAuthServiceFactory = ({ const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -394,28 +404,32 @@ export const identityTlsCertAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) { throw new BadRequestError({ message: "The identity does not have TLS Certificate auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission, memberships } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission, memberships } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + scope: OrganizationActionScope.Any + }); const shouldUseNewPrivilegeSystem = Boolean(memberships?.[0]?.shouldUseNewPrivilegeSystem); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, 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 eb9f4ab5d..cf35bb5ee 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, TIdentityTlsCertAuths, TMemberships } from "@app/db/schemas"; +import { TIdentities, TIdentityAccessTokens, TIdentityTlsCertAuths } 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: TMemberships; + identity: TIdentities; }>; 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 2ae05cb97..2d3e11cd8 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 { AccessScope, IdentityAuthMethod, TableName } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope, 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 { @@ -10,10 +10,17 @@ import { import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; -import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -32,11 +39,12 @@ import { } from "./identity-token-auth-types"; type TIdentityTokenAuthServiceFactoryDep = { + identityDAL: Pick; identityTokenAuthDAL: Pick< TIdentityTokenAuthDALFactory, "transaction" | "create" | "findOne" | "updateById" | "delete" >; - membershipIdentityDAL: Pick; + membershipIdentityDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" @@ -49,8 +57,8 @@ type TIdentityTokenAuthServiceFactoryDep = { export type TIdentityTokenAuthServiceFactory = ReturnType; export const identityTokenAuthServiceFactory = ({ + identityDAL, identityTokenAuthDAL, - // identityDAL, membershipIdentityDAL, identityAccessTokenDAL, permissionService, @@ -79,6 +87,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -90,13 +101,14 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -155,6 +167,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -172,13 +187,14 @@ export const identityTokenAuthServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -223,6 +239,9 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ @@ -232,13 +251,14 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityTokenAuth, orgId: identityMembershipOrg.scopeOrgId }; @@ -262,28 +282,33 @@ export const identityTokenAuthServiceFactory = ({ identityId }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) { throw new BadRequestError({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -341,22 +366,26 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -379,13 +408,13 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); + const identity = await identityDAL.findById(identityTokenAuth.identityId); + if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, - { - lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, - lastLoginTime: new Date() - }, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, lastLoginTime: new Date() }, tx ); const newToken = await identityAccessTokenDAL.create( @@ -420,7 +449,7 @@ export const identityTokenAuthServiceFactory = ({ } ); - return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg }; + return { accessToken, identityTokenAuth, identityAccessToken, identity }; }; const getTokenAuthTokens = async ({ @@ -449,13 +478,14 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const tokens = await identityAccessTokenDAL.find( @@ -501,22 +531,24 @@ export const identityTokenAuthServiceFactory = ({ message: "The identity does not have Token Auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -580,13 +612,14 @@ export const identityTokenAuthServiceFactory = ({ throw new NotFoundError({ message: `Failed to find identity with ID ${identityAccessToken.identityId}` }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const [revokedToken] = await identityAccessTokenDAL.update( diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 563a3f897..00ab1610d 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 { AccessScope, IdentityAuthMethod } from "@app/db/schemas"; +import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { @@ -13,6 +13,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, + ForbiddenRequestError, NotFoundError, PermissionBoundaryError, RateLimitError, @@ -22,6 +23,7 @@ import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from import { logger } from "@app/lib/logger"; import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-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"; @@ -42,6 +44,7 @@ import { } from "./identity-ua-types"; type TIdentityUaServiceFactoryDep = { + identityDAL: Pick; identityUaDAL: TIdentityUaDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory; @@ -70,7 +73,8 @@ export const identityUaServiceFactory = ({ permissionService, licenseService, orgDAL, - keyStore + keyStore, + identityDAL }: TIdentityUaServiceFactoryDep) => { const login = async (clientId: string, clientSecret: string, ip: string) => { const identityUa = await identityUaDAL.findOne({ clientId }); @@ -100,16 +104,6 @@ export const identityUaServiceFactory = ({ }); } - const identityMembershipOrg = await membershipIdentityDAL.findOne({ - actorIdentityId: identityUa.identityId, - scope: AccessScope.Organization - }); - if (!identityMembershipOrg) { - throw new UnauthorizedError({ - message: "Invalid credentials" - }); - } - const clientSecretPrefix = clientSecret.slice(0, 4); const clientSecretInfo = await identityUaClientSecretDAL.find({ identityUAId: identityUa.id, @@ -227,10 +221,11 @@ export const identityUaServiceFactory = ({ accessTokenMaxTTL: 1000000000 }; + const identity = await identityDAL.findById(identityUa.identityId); const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - await membershipIdentityDAL.updateById( - identityMembershipOrg.id, + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, lastLoginTime: new Date() @@ -276,7 +271,7 @@ export const identityUaServiceFactory = ({ identityUa, validClientSecretInfo, identityAccessToken, - identityMembershipOrg, + identity, ...accessTokenTTLParams }; }; @@ -315,18 +310,23 @@ export const identityUaServiceFactory = ({ message: "Failed to add universal auth to already configured identity" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -423,6 +423,10 @@ export const identityUaServiceFactory = ({ }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + if ( (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 && (accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) @@ -430,13 +434,14 @@ export const identityUaServiceFactory = ({ throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.scopeOrgId); @@ -512,14 +517,18 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...uaIdentityAuth, orgId: identityMembershipOrg.scopeOrgId }; }; @@ -545,22 +554,27 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -611,23 +625,28 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -692,23 +711,28 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } + + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); - + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -761,6 +785,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityUa = await identityUaDAL.findOne({ identityId }); if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -768,22 +795,24 @@ 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 } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( shouldUseNewPrivilegeSystem, @@ -828,6 +857,9 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } const identityUa = await identityUaDAL.findOne({ identityId }); if (!identityUa) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); @@ -835,22 +867,24 @@ 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 } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - identityMembershipOrg.identity.id, - identityMembershipOrg.scopeOrgId, + const { permission: rolePermission } = await permissionService.getOrgPermission({ + actor: ActorType.IDENTITY, + actorId: identityMembershipOrg.identity.id, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(identityMembershipOrg.scopeOrgId); const permissionBoundary = validatePrivilegeChangeOperation( @@ -900,14 +934,18 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } + if (identityMembershipOrg.identity.identityOrgId !== actorOrgId) { + throw new ForbiddenRequestError({ message: "Sub organization not authorized to access this identity" }); + } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityMembershipOrg.scopeOrgId, + orgId: identityMembershipOrg.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const deleted = await keyStore.deleteItems({ diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 65aee561c..66556f5fa 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -163,7 +163,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { .select( selectAllTableCols(TableName.Membership), db.ref("name").withSchema(TableName.Identity).as("identityName"), - db.ref("hasDeleteProtection").withSchema(TableName.Identity) + db.ref("hasDeleteProtection").withSchema(TableName.Identity), + db.ref("orgId").withSchema(TableName.Identity) ) .where(filter) .as("paginatedIdentity"); @@ -257,6 +258,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("customRoleId").withSchema(TableName.MembershipRole).as("roleId"), db.ref("scopeOrgId").withSchema("paginatedIdentity").as("orgId"), db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"), + db.ref("orgId").withSchema("paginatedIdentity").as("identityOrgId"), db.ref("lastLoginTime").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), @@ -309,6 +311,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { roleId, id, orgId, + identityOrgId, uaId, alicloudId, awsId, @@ -348,6 +351,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id: identityId as string, name: identityName, hasDeleteProtection, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, @@ -515,6 +519,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("actorIdentityId").withSchema(TableName.Membership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity), + db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), @@ -566,6 +571,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { crPermission, crName, identityId, + identityOrgId, identityName, hasDeleteProtection, role, @@ -611,6 +617,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id: identityId as string, name: identityName, hasDeleteProtection, + orgId: identityOrgId, authMethods: buildAuthMethods({ uaId, alicloudId, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 721844070..f6ec60e9e 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 { AccessScope, OrgMembershipRole, TableName, TRoles } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, 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,7 @@ 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 { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TOrgDALFactory } from "../org/org-dal"; @@ -40,6 +41,7 @@ type TIdentityServiceFactoryDep = { licenseService: Pick; keyStore: Pick; orgDAL: Pick; + additionalPrivilegeDAL: Pick; }; export type TIdentityServiceFactory = ReturnType; @@ -54,7 +56,8 @@ export const identityServiceFactory = ({ keyStore, orgDAL, membershipIdentityDAL, - membershipRoleDAL + membershipRoleDAL, + additionalPrivilegeDAL }: TIdentityServiceFactoryDep) => { const createIdentity = async ({ name, @@ -67,7 +70,14 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const [rolePermissionDetails] = await permissionService.getOrgPermissionByRoles([role], orgId); @@ -104,7 +114,7 @@ export const identityServiceFactory = ({ } const identity = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx); + const newIdentity = await identityDAL.create({ name, hasDeleteProtection, orgId }, tx); const membership = await membershipIdentityDAL.create( { scope: AccessScope.Organization, @@ -172,13 +182,14 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); let customRole: TRoles | undefined; @@ -208,11 +219,12 @@ export const identityServiceFactory = ({ if (isCustomRole) customRole = rolePermissionDetails?.role; } + const identityDetails = await identityDAL.findById(id); const identity = await identityDAL.transaction(async (tx) => { const newIdentity = - name || hasDeleteProtection + identityDetails.orgId === actorOrgId && (name || hasDeleteProtection) ? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx) - : await identityDAL.findById(id, tx); + : identityDetails; if (role) { await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx); @@ -264,16 +276,16 @@ export const identityServiceFactory = ({ const identity = doc[0]; if (!identity) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identity.orgId, + orgId: identity.orgId, actorAuthMethod, actorOrgId - ); + }); 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(); @@ -314,23 +326,56 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); if (identityOrgMembership.identity.hasDeleteProtection) throw new BadRequestError({ message: "Identity has delete protection" }); - const deletedIdentity = await identityDAL.deleteById(id); + if (identityOrgMembership.identity.identityOrgId === actorOrgId) { + const deletedIdentity = await identityDAL.deleteById(id); + await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); + return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; + } - await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.scopeOrgId); + await membershipIdentityDAL.transaction(async (tx) => { + await identityMetadataDAL.delete( + { + identityId: id, + orgId: actorOrgId + }, + tx + ); + const identityProjectMembership = await membershipIdentityDAL.find( + { + actorIdentityId: id, + scope: AccessScope.Project, + scopeOrgId: actorOrgId + }, + { tx } + ); + await additionalPrivilegeDAL.delete( + { + actorIdentityId: id, + $in: { + projectId: identityProjectMembership.map((el) => el.scopeProjectId) + } + }, + tx + ); + const doc = await membershipIdentityDAL.delete({ actorIdentityId: id, scopeOrgId: actorOrgId }, tx); + return doc; + }); + const deletedIdentity = await identityDAL.findById(id); return { ...deletedIdentity, orgId: identityOrgMembership.scopeOrgId }; }; @@ -346,7 +391,14 @@ export const identityServiceFactory = ({ orderDirection, search }: TListOrgIdentitiesByOrgIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ @@ -379,7 +431,14 @@ export const identityServiceFactory = ({ orderDirection, searchFilter = {} }: TSearchOrgIdentitiesByOrgIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId: actorOrgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { totalCount, docs } = await identityOrgMembershipDAL.searchIdentities({ @@ -408,16 +467,17 @@ export const identityServiceFactory = ({ }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - identityOrgMembership.scopeOrgId, + orgId: identityOrgMembership.scopeOrgId, actorAuthMethod, actorOrgId - ); + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); - const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); + const identityMemberships = await identityProjectDAL.findByIdentityId(identityId, actorOrgId); return identityMemberships; }; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 248488e9f..1d1f2cbb6 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -112,7 +112,7 @@ export const integrationAuthServiceFactory = ({ }; const listOrgIntegrationAuth = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGenericPermission) => { - const authorizations = await integrationAuthDAL.getByOrg(actorOrgId as string); + const authorizations = await integrationAuthDAL.getByOrg(actorOrgId); const filteredAuthorizations = await Promise.all( authorizations.map(async (auth) => { 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 index 1e87ee3ca..d69db9c08 100644 --- a/backend/src/services/membership-group/org/org-membership-group-factory.ts +++ b/backend/src/services/membership-group/org/org-membership-group-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, @@ -45,13 +45,14 @@ export const newOrgMembershipGroupFactory = ({ }; 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); const permissionRoles = await permissionService.getOrgPermissionByRoles( dto.data.roles.map((el) => el.role), @@ -89,26 +90,28 @@ export const newOrgMembershipGroupFactory = ({ }; 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); }; diff --git a/backend/src/services/membership-identity/membership-identity-dal.ts b/backend/src/services/membership-identity/membership-identity-dal.ts index 64e508fb8..682bfef3e 100644 --- a/backend/src/services/membership-identity/membership-identity-dal.ts +++ b/backend/src/services/membership-identity/membership-identity-dal.ts @@ -91,6 +91,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { .select( db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("id").withSchema(TableName.Identity).as("identityId"), + db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"), db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"), db.ref("slug").withSchema(TableName.Role).as("roleSlug"), @@ -132,6 +133,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { parentMapper: (el) => { const { identityId: actorIdentityId, + identityOrgId, identityHasDeleteProtection, identityName, uaId, @@ -153,6 +155,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { name: identityName, id: actorIdentityId, hasDeleteProtection: identityHasDeleteProtection, + identityOrgId, authMethods: buildAuthMethods({ uaId, awsId, @@ -353,5 +356,34 @@ export const membershipIdentityDALFactory = (db: TDbClient) => { } }; - return { ...orm, findIdentities, getIdentityById }; + // this right now only support sub organization + const listAvailableIdentities = async (orgId: string, rootOrgId: string) => { + try { + const usersConnectedToOrg = db + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .select("actorIdentityId"); + + const docs = await db + .replicaNode()(TableName.Membership) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNotNull(`${TableName.Membership}.actorIdentityId`) + .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) + .whereNotIn(`${TableName.Membership}.actorIdentityId`, usersConnectedToOrg) + .select( + db.ref("id").withSchema(TableName.Identity), + db.ref("name").withSchema(TableName.Identity), + db.ref("hasDeleteProtection").withSchema(TableName.Identity) + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "ListAvailableIdentities" }); + } + }; + + return { ...orm, findIdentities, getIdentityById, listAvailableIdentities }; }; diff --git a/backend/src/services/membership-identity/membership-identity-service.ts b/backend/src/services/membership-identity/membership-identity-service.ts index 4dd3da064..16292ea82 100644 --- a/backend/src/services/membership-identity/membership-identity-service.ts +++ b/backend/src/services/membership-identity/membership-identity-service.ts @@ -6,6 +6,7 @@ import { ms } from "@app/lib/ms"; import { SearchResourceOperators } from "@app/lib/search-resource/search"; import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal"; +import { TIdentityDALFactory } from "../identity/identity-dal"; import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; import { TOrgDALFactory } from "../org/org-dal"; import { TRoleDALFactory } from "../role/role-dal"; @@ -31,6 +32,7 @@ type TMembershipIdentityServiceFactoryDep = { >; orgDAL: Pick; additionalPrivilegeDAL: Pick; + identityDAL: Pick; }; export type TMembershipIdentityServiceFactory = ReturnType; @@ -41,12 +43,14 @@ export const membershipIdentityServiceFactory = ({ membershipRoleDAL, permissionService, orgDAL, - additionalPrivilegeDAL + additionalPrivilegeDAL, + identityDAL }: TMembershipIdentityServiceFactoryDep) => { const scopeFactory = { [AccessScope.Organization]: newOrgMembershipIdentityFactory({ orgDAL, - permissionService + permissionService, + identityDAL }), [AccessScope.Project]: newProjectMembershipIdentityFactory({ membershipIdentityDAL, @@ -305,7 +309,7 @@ export const membershipIdentityServiceFactory = ({ [SearchResourceOperators.$contains]: dto.data.identityName } : undefined, - role: dto.data.roles.length + role: dto.data?.roles?.length ? { [SearchResourceOperators.$in]: dto.data.roles } @@ -329,11 +333,29 @@ export const membershipIdentityServiceFactory = ({ return membership; }; + const listAvailableIdentities = async (dto: TListMembershipIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipIdentityGuard(dto); + + const organizationDetails = await orgDAL.findById(dto.scopeData.orgId); + if (!organizationDetails.rootOrgId) return { identities: [] }; + + const identities = await membershipIdentityDAL.listAvailableIdentities( + organizationDetails.id, + organizationDetails.rootOrgId + ); + + return { identities }; + }; + return { createMembership, updateMembership, deleteMembership, listMemberships, - getMembershipByIdentityId + getMembershipByIdentityId, + listAvailableIdentities }; }; diff --git a/backend/src/services/membership-identity/membership-identity-types.ts b/backend/src/services/membership-identity/membership-identity-types.ts index adce10237..78923cb14 100644 --- a/backend/src/services/membership-identity/membership-identity-types.ts +++ b/backend/src/services/membership-identity/membership-identity-types.ts @@ -54,14 +54,11 @@ export type TUpdateMembershipIdentityDTO = { export type TListMembershipIdentityDTO = { permission: OrgServiceActor; scopeData: AccessScopeData; - selector: { - identityId: string; - }; data: { limit?: number; offset?: number; identityName?: string; - roles: string[]; + roles?: string[]; }; }; 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 index 06789e274..8b3bdf6d5 100644 --- a/backend/src/services/membership-identity/org/org-membership-identity-factory.ts +++ b/backend/src/services/membership-identity/org/org-membership-identity-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, @@ -8,6 +8,7 @@ import { } 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 { TIdentityDALFactory } from "@app/services/identity/identity-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { isCustomOrgRole } from "@app/services/org/org-role-fns"; @@ -16,11 +17,13 @@ import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; type TOrgMembershipIdentityScopeFactoryDep = { permissionService: Pick; orgDAL: Pick; + identityDAL: Pick; }; export const newOrgMembershipIdentityFactory = ({ permissionService, - orgDAL + orgDAL, + identityDAL }: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { if (dto.scope === AccessScope.Organization) { @@ -38,23 +41,66 @@ export const newOrgMembershipIdentityFactory = ({ 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 onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const identityDetails = await identityDAL.findById(dto.data.identityId); + if (identityDetails.orgId !== dto.permission.rootOrgId) { + throw new BadRequestError({ message: "Only identities from parent organization can be invited" }); + } + + 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 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const permissionRoles = await permissionService.getOrgPermissionByRoles( dto.data.roles.map((el) => el.role), @@ -85,35 +131,54 @@ export const newOrgMembershipIdentityFactory = ({ } }; - const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = - async () => { - throw new BadRequestError({ - message: "Organization membership cannot be deleted for organization scoped identity" - }); - }; + const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async ( + dto + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); + + const identityDetails = await identityDAL.findById(dto.selector.identityId); + if (identityDetails.orgId !== dto.permission.rootOrgId) { + throw new BadRequestError({ message: "Only identities from parent organization can do this operation" }); + } + + if (identityDetails.orgId === dto.permission.orgId) { + throw new BadRequestError({ message: "Identity cannot exist as orphan" }); + } + }; 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); }; diff --git a/backend/src/services/membership-user/membership-user-dal.ts b/backend/src/services/membership-user/membership-user-dal.ts index 7882b9639..17970f16f 100644 --- a/backend/src/services/membership-user/membership-user-dal.ts +++ b/backend/src/services/membership-user/membership-user-dal.ts @@ -291,5 +291,37 @@ export const membershipUserDALFactory = (db: TDbClient) => { } }; - return { ...orm, findUsers, getUserById }; + // this right now only support sub organization + const listAvailableUsers = async (orgId: string, rootOrgId: string) => { + try { + const usersConnectedToOrg = db + .replicaNode()(TableName.Membership) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Membership}.scopeOrgId`, orgId) + .select("actorUserId"); + + const docs = await db + .replicaNode()(TableName.Membership) + .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .where(`${TableName.Users}.isGhost`, false) + .whereNotNull(`${TableName.Membership}.actorUserId`) + .where(`${TableName.Membership}.scopeOrgId`, rootOrgId) + .whereNotIn(`${TableName.Membership}.actorUserId`, usersConnectedToOrg) + .select( + db.ref("id").withSchema(TableName.Users), + db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), + db.ref("firstName").withSchema(TableName.Users), + db.ref("lastName").withSchema(TableName.Users) + ); + + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "ListAvailableUsers" }); + } + }; + + return { ...orm, findUsers, getUserById, listAvailableUsers }; }; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index 82dca0159..4b14ee771 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -40,7 +40,7 @@ import { newProjectMembershipUserFactory } from "./project/project-membership-us type TMembershipUserServiceFactoryDep = { membershipUserDAL: TMembershipUserDALFactory; membershipRoleDAL: Pick; - orgDAL: Pick; + orgDAL: Pick; roleDAL: Pick; userDAL: TUserDALFactory; permissionService: Pick< @@ -83,7 +83,8 @@ export const membershipUserServiceFactory = ({ orgDAL, tokenService, userDAL, - userGroupMembershipDAL + userGroupMembershipDAL, + membershipUserDAL }), [AccessScope.Namespace]: newNamespaceMembershipUserFactory({}), [AccessScope.Project]: newProjectMembershipUserFactory({ @@ -404,7 +405,7 @@ export const membershipUserServiceFactory = ({ const membershipDoc = await membershipUserDAL.transaction(async (tx) => { if (dto.scopeData.scope === AccessScope.Organization) { const [doc] = await deleteOrgMembershipsFn({ - orgMembershipIds: [], + orgMembershipIds: [existingMembership.id], orgId: dto.permission.orgId, orgDAL, projectKeyDAL, @@ -471,11 +472,26 @@ export const membershipUserServiceFactory = ({ return membership; }; + // Should only be used for sub organization as of now + const listAvailableUsers = async (dto: TListMembershipUserDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListMembershipUserGuard(dto); + + const organizationDetails = await orgDAL.findById(dto.scopeData.orgId); + if (!organizationDetails.rootOrgId) return { users: [] }; + + const users = await membershipUserDAL.listAvailableUsers(organizationDetails.id, organizationDetails.rootOrgId); + return { users }; + }; + return { createMembership, updateMembership, deleteMembership, listMemberships, - getMembershipByUserId + getMembershipByUserId, + listAvailableUsers }; }; diff --git a/backend/src/services/membership-user/membership-user-types.ts b/backend/src/services/membership-user/membership-user-types.ts index b8761671c..15982bb6a 100644 --- a/backend/src/services/membership-user/membership-user-types.ts +++ b/backend/src/services/membership-user/membership-user-types.ts @@ -93,3 +93,8 @@ export type TGetMembershipUserByUserIdDTO = { userId: string; }; }; + +export type TListAvailableUsersDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; +}; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index 523e85bae..d21b27b69 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } 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"; @@ -15,6 +15,7 @@ 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 { TMembershipUserDALFactory } from "../membership-user-dal"; import { TMembershipUserScopeFactory } from "../membership-user-types"; type TOrgMembershipUserScopeFactoryDep = { @@ -25,6 +26,7 @@ type TOrgMembershipUserScopeFactoryDep = { orgDAL: Pick; userGroupMembershipDAL: Pick; licenseService: Pick; + membershipUserDAL: Pick; }; export const newOrgMembershipUserFactory = ({ @@ -33,7 +35,8 @@ export const newOrgMembershipUserFactory = ({ userDAL, orgDAL, smtpService, - licenseService + licenseService, + membershipUserDAL }: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { if (dto.scope === AccessScope.Organization) { @@ -51,14 +54,18 @@ export const newOrgMembershipUserFactory = ({ 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 - ); + const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async ( + dto, + newMembers + ) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); const plan = await licenseService.getPlan(dto.permission.orgId); @@ -77,6 +84,25 @@ export const newOrgMembershipUserFactory = ({ message: "Failed to invite user due to org-level auth enforced for organization" }); } + + if (org.rootOrgId) { + const rootOrgMembership = await membershipUserDAL.find({ + scope: AccessScope.Organization, + $in: { + actorUserId: newMembers.map((el) => el.id) + }, + scopeOrgId: org.rootOrgId + }); + if (rootOrgMembership.length !== newMembers.length) { + const emails = newMembers + .filter((user) => !rootOrgMembership.find((i) => i.actorUserId === user.id)) + .map((el) => el.email) + .join(","); + throw new BadRequestError({ + message: `Users with email ${emails} doesn't have membership in root organization` + }); + } + } }; const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( @@ -95,87 +121,103 @@ export const newOrgMembershipUserFactory = ({ const signUpTokens: { email: string; link: string }[] = []; const orgDetails = await orgDAL.findById(dto.permission.orgId); + if (orgDetails.rootOrgId) { + const emails = newUsers.map((el) => el.email).filter(Boolean); + await smtpService.sendMail({ + template: SmtpTemplates.SubOrgInvite, + subjectLine: "Infisical sub-organization invitation", + recipients: emails as string[], + substitutions: { + subOrganizationName: orgDetails.slug, + callback_url: `${appCfg.SITE_URL}/organization/projects?subOrganization=${orgDetails.slug}` + } + }); + } else { + 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 + }); - 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}` + }); + } - 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` + } }); } - - 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); }; diff --git a/backend/src/services/microsoft-teams/microsoft-teams-service.ts b/backend/src/services/microsoft-teams/microsoft-teams-service.ts index 23ed61402..ff17daa75 100644 --- a/backend/src/services/microsoft-teams/microsoft-teams-service.ts +++ b/backend/src/services/microsoft-teams/microsoft-teams-service.ts @@ -9,6 +9,7 @@ import { import { CronJob } from "cron"; import { FastifyReply, FastifyRequest } from "fastify"; +import { OrganizationActionScope } 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 { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; @@ -208,13 +209,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -282,13 +284,14 @@ export const microsoftTeamsServiceFactory = ({ description, redirectUri }: TCreateMicrosoftTeamsIntegrationDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -393,13 +396,14 @@ export const microsoftTeamsServiceFactory = ({ }); }; const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -427,13 +431,14 @@ export const microsoftTeamsServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetMicrosoftTeamsIntegrationByOrgDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -463,13 +468,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -495,13 +501,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -549,13 +556,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); @@ -577,13 +585,14 @@ export const microsoftTeamsServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - microsoftTeamsIntegration.orgId, + orgId: microsoftTeamsIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 4c080717d..4f4a9b08c 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, 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"; @@ -44,13 +44,14 @@ export const orgAdminServiceFactory = ({ actorOrgId, actorAuthMethod }: TListOrgProjectsDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole @@ -76,13 +77,14 @@ export const orgAdminServiceFactory = ({ actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index d987c890c..c2565f36b 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -26,6 +26,7 @@ import { } from "@app/lib/knex"; import { generateKnexQueryFromScim } from "@app/lib/knex/scim"; +import { ActorType } from "../auth/auth-type"; import { OrgAuthMethod } from "./org-types"; export type TOrgDALFactory = ReturnType; @@ -64,6 +65,7 @@ export const orgDALFactory = (db: TDbClient) => { const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { return db .replicaNode()(TableName.Organization) + .whereNull(`${TableName.Organization}.rootOrgId`) .whereIn(`${TableName.Organization}.id`, orgIdSubquery) .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) @@ -154,11 +156,49 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const listSubOrganizations = async (dto: { + actorId: string; + actorType: ActorType; + orgId: string; + isAccessible?: boolean; + limit?: number; + offset?: number; + }) => { + try { + // TODO(sub-org:group): check this when implement group support + const query = db + .replicaNode()(TableName.Organization) + .where(`${TableName.Organization}.rootOrgId`, dto.orgId) + .select(selectAllTableCols(TableName.Organization)); + + if (dto.isAccessible) { + void query + .leftJoin(`${TableName.Membership}`, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .where((qb) => { + void qb.where(`${TableName.Membership}.scope`, AccessScope.Organization); + if (dto.actorType === ActorType.IDENTITY) { + void qb.andWhere(`${TableName.Membership}.actorIdentityId`, dto.actorId); + } else { + void qb.andWhere(`${TableName.Membership}.actorUserId`, dto.actorId); + } + }); + } + if (dto.limit) void query.limit(dto.limit); + if (dto.offset) void query.offset(dto.offset); + + const orgs = await query; + return orgs; + } catch (error) { + throw new DatabaseError({ error, name: "List sub organization" }); + } + }; + const findOrgById = async (orgId: string) => { try { const org = (await db .replicaNode()(TableName.Organization) .where({ [`${TableName.Organization}.id` as "id"]: orgId }) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -195,6 +235,7 @@ export const orgDALFactory = (db: TDbClient) => { try { const org = (await db .replicaNode()(TableName.Organization) + .whereNull(`${TableName.Organization}.rootOrgId`) .where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug }) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( @@ -240,6 +281,7 @@ export const orgDALFactory = (db: TDbClient) => { .whereNotNull(`${TableName.Membership}.actorUserId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.SamlConfig, (qb) => { qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( `${TableName.SamlConfig}.isActive`, @@ -337,6 +379,7 @@ export const orgDALFactory = (db: TDbClient) => { } }; + // TODO(sub-org): updated this logic later const countAllOrgMembers = async (orgId: string) => { try { interface CountResult { @@ -610,6 +653,7 @@ export const orgDALFactory = (db: TDbClient) => { }) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) + .whereNull(`${TableName.Organization}.rootOrgId`) .leftJoin(TableName.UserAliases, function joinUserAlias() { this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) @@ -648,6 +692,7 @@ export const orgDALFactory = (db: TDbClient) => { .replicaNode()(TableName.Membership) .where({ actorIdentityId: identityId }) .where(`${TableName.Membership}.scope`, AccessScope.Organization) + .whereNull(`${TableName.Organization}.rootOrgId`) .whereNotNull(`${TableName.Membership}.actorIdentityId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) @@ -662,11 +707,30 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const findRootOrgDetails = async (orgId: string, tx?: Knex): Promise => { + try { + const org = await (tx ?? db.replicaNode())(TableName.Organization) + .select(selectAllTableCols(TableName.Organization)) + .where( + "id", + db(TableName.Organization) + .select(db.raw(`CASE WHEN "rootOrgId" IS NULL THEN id ELSE "rootOrgId" END`)) + .where("id", orgId) + ) + .first(); + + return org; + } catch (error) { + throw new DatabaseError({ error, name: "FindRootOrgDetails" }); + } + }; + return withTransaction(db, { ...orgOrm, findOrgByProjectId, findAllOrgMembers, countAllOrgMembers, + listSubOrganizations, findOrgById, findOrgBySlug, findAllOrgsByUserId, @@ -684,6 +748,7 @@ export const orgDALFactory = (db: TDbClient) => { deleteMembershipById, deleteMembershipsById, updateMembership, - findIdentityOrganization + findIdentityOrganization, + findRootOrgDetails }); }; diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 78d52e816..b887eeea1 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -13,7 +13,7 @@ import { TMembershipUserDALFactory } from "../membership-user/membership-user-da type TDeleteOrgMemberships = { orgMembershipIds: string[]; orgId: string; - orgDAL: Pick; + orgDAL: Pick; userGroupMembershipDAL: Pick; membershipUserDAL: Pick; membershipRoleDAL: Pick; @@ -34,19 +34,9 @@ export const deleteOrgMembershipsFn = async ({ userId, membershipUserDAL, userGroupMembershipDAL, - membershipRoleDAL, additionalPrivilegeDAL }: TDeleteOrgMemberships) => { const deletedMemberships = await orgDAL.transaction(async (tx) => { - await membershipRoleDAL.delete( - { - $in: { - membershipId: orgMembershipIds - } - }, - tx - ); - const orgMemberships = await membershipUserDAL.delete( { scopeOrgId: orgId, @@ -83,12 +73,13 @@ export const deleteOrgMembershipsFn = async ({ ); // Get all the project memberships of the users in the organization + const childOrgs = await orgDAL.find({ rootOrgId: orgId }, { tx }); // Delete all the project memberships of the users in the organization const otherMemberships = await membershipUserDAL.delete( { - scopeOrgId: orgId, $in: { + scopeOrgId: [orgId].concat(childOrgs.map((el) => el.id)), actorUserId: membershipUserIds } }, @@ -96,7 +87,9 @@ export const deleteOrgMembershipsFn = async ({ ); const orgGroups = await membershipUserDAL.find({ - scopeOrgId: orgId, + $in: { + scopeOrgId: [orgId].concat(childOrgs.map((el) => el.id)) + }, $notNull: ["actorGroupId"] }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5b98b44b1..20ed37a06 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -4,6 +4,7 @@ import { Knex } from "knex"; import { AccessScope, + OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TableName, @@ -156,16 +157,31 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + rootOrgId: string, + actorOrgId: string ) => { - await permissionService.getOrgPermission(ActorType.USER, userId, orgId, actorAuthMethod, actorOrgId); + await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, + orgId, + actorAuthMethod, + actorOrgId: rootOrgId, + scope: OrganizationActionScope.Any + }); const appCfg = getConfig(); const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); - if (!org.userTokenExpiration) { - return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME }; + + const hasSubOrg = actorOrgId !== rootOrgId; + let subOrg; + if (hasSubOrg) { + subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId }); } - return org; + + if (!org.userTokenExpiration) { + return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg }; + } + return { ...org, subOrganization: subOrg }; }; /* * Get all organization a user part of @@ -192,15 +208,16 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findAllOrgMembers(orgId); @@ -208,7 +225,14 @@ export const orgServiceFactory = ({ }; const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const groups = await groupDAL.findByOrgId(orgId); return groups; @@ -222,7 +246,14 @@ export const orgServiceFactory = ({ orgId, emails }: TFindOrgMembersByEmailDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const members = await orgDAL.findOrgMembersByUsername(orgId, emails); @@ -309,13 +340,14 @@ export const orgServiceFactory = ({ actorAuthMethod, orgId }: TUpgradePrivilegeSystemDTO) => { - const { hasRole } = await permissionService.getOrgPermission( - ActorType.USER, + const { hasRole } = await permissionService.getOrgPermission({ + actor: ActorType.USER, actorId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ @@ -380,7 +412,14 @@ export const orgServiceFactory = ({ } }: TUpdateOrgDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); if (allowSecretSharingOutsideOrganization !== undefined) { @@ -477,6 +516,12 @@ export const orgServiceFactory = ({ } } + if (slug) { + const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null }); + if (existingOrg && existingOrg?.id !== orgId) + throw new BadRequestError({ message: `Organization with slug ${slug} already exist` }); + } + if (googleSsoAuthEnforced) { if (googleSsoAuthEnforced && currentOrg.authEnforced) { throw new BadRequestError({ @@ -634,7 +679,7 @@ export const orgServiceFactory = ({ const organization = await (trx ? createOrg(trx) : orgDAL.transaction(createOrg)); - await licenseService.updateSubscriptionOrgMemberCount(organization.id); + await licenseService.updateSubscriptionOrgMemberCount(organization.id, trx); return organization; }; @@ -656,15 +701,16 @@ export const orgServiceFactory = ({ ipAddress: string; orgId: string; actorAuthMethod: ActorAuthMethod; - actorOrgId: string | undefined; + actorOrgId: string; }) => { - const { hasRole } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { hasRole } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!hasRole(OrgMembershipRole.Admin)) { throw new ForbiddenRequestError({ name: "DeleteOrganizationById", @@ -744,13 +790,14 @@ export const orgServiceFactory = ({ actorOrgId, metadata }: TUpdateOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member); const foundMembership = await membershipUserDAL.findOne({ @@ -831,7 +878,14 @@ export const orgServiceFactory = ({ membershipId }: TResendOrgMemberInvitationDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); @@ -967,7 +1021,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TGetOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const membership = await orgMembershipDAL.findOrgMembershipById(membershipId); @@ -988,13 +1049,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); const [deletedMembership] = await deleteOrgMembershipsFn({ @@ -1021,13 +1083,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TDeleteOrgMembershipsDTO) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); if (membershipIds.includes(userId)) { @@ -1059,7 +1122,14 @@ export const orgServiceFactory = ({ actorAuthMethod, actorOrgId }: TListProjectMembershipsByOrgMembershipIdDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); const membership = await orgMembershipDAL.findOrgMembershipById(orgMembershipId); @@ -1080,15 +1150,16 @@ export const orgServiceFactory = ({ userId: string, orgId: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); const incidentContacts = await incidentContactDAL.findByOrgId(orgId); return incidentContacts; @@ -1099,15 +1170,16 @@ export const orgServiceFactory = ({ orgId: string, email: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount); const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email }); if (doesIncidentContactExist) { @@ -1126,15 +1198,16 @@ export const orgServiceFactory = ({ orgId: string, id: string, actorAuthMethod: ActorAuthMethod, - actorOrgId: string | undefined + actorOrgId: string ) => { - const { permission } = await permissionService.getOrgPermission( - ActorType.USER, - userId, + const { permission } = await permissionService.getOrgPermission({ + actor: ActorType.USER, + actorId: userId, orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.ParentOrganization + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount); const incidentContact = await incidentContactDAL.deleteById(id, orgId); diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 1a27d131f..48680456c 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -8,7 +8,7 @@ export type TUpdateOrgMembershipDTO = { membershipId: string; role?: string; isActive?: boolean; - actorOrgId: string | undefined; + actorOrgId: string; metadata?: { key: string; value: string }[]; actorAuthMethod: ActorAuthMethod; }; @@ -21,7 +21,7 @@ export type TDeleteOrgMembershipDTO = { userId: string; orgId: string; membershipId: string; - actorOrgId: string | undefined; + actorOrgId: string; actorAuthMethod: ActorAuthMethod; }; @@ -29,7 +29,7 @@ export type TDeleteOrgMembershipsDTO = { userId: string; orgId: string; membershipIds: string[]; - actorOrgId: string | undefined; + actorOrgId: string; actorAuthMethod: ActorAuthMethod; }; @@ -54,7 +54,7 @@ export type TVerifyUserToOrgDTO = { export type TFindOrgMembersByEmailDTO = { actor: ActorType; - actorOrgId: string | undefined; + actorOrgId: string; actorId: string; actorAuthMethod: ActorAuthMethod; orgId: string; @@ -64,7 +64,7 @@ export type TFindOrgMembersByEmailDTO = { export type TFindAllWorkspacesDTO = { actor: ActorType; actorId: string; - actorOrgId: string | undefined; + actorOrgId: string; actorAuthMethod: ActorAuthMethod; orgId: string; }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 2abdebdbc..11d4239db 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -413,10 +413,12 @@ export const projectDALFactory = (db: TDbClient) => { const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => { try { + const subOrgProjects = db.replicaNode()(TableName.Organization).where({ rootOrgId: orgId }).select("id"); + const doc = await (tx || db.replicaNode())(TableName.Project) .andWhere((bd) => { if (orgId) { - void bd.where({ orgId }); + void bd.where({ orgId }).orWhereIn("orgId", subOrgProjects); } }) .count(); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 58b3c5395..e29f18404 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -5,6 +5,7 @@ import slugify from "@sindresorhus/slugify"; import { AccessScope, ActionProjectType, + OrganizationActionScope, ProjectMembershipRole, ProjectType, ProjectVersion, @@ -245,13 +246,14 @@ export const projectServiceFactory = ({ type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - organization.id, + orgId: organization.id, actorAuthMethod, actorOrgId - ); + }); if ( permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace) && @@ -513,13 +515,14 @@ export const projectServiceFactory = ({ : await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); // `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); @@ -1822,13 +1825,14 @@ export const projectServiceFactory = ({ projectIds }: TSearchProjectsDTO) => { // check user belong to org - await permissionService.getOrgPermission( - permission.type, - permission.id, - permission.orgId, - permission.authMethod, - permission.orgId - ); + await permissionService.getOrgPermission({ + actor: permission.type, + actorId: permission.id, + orgId: permission.orgId, + actorAuthMethod: permission.authMethod, + scope: OrganizationActionScope.Any, + actorOrgId: permission.orgId + }); return projectDAL.searchProjects({ limit, @@ -1846,13 +1850,14 @@ export const projectServiceFactory = ({ const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => { // check user belong to org - await permissionService.getOrgPermission( - permission.type, - permission.id, - permission.orgId, - permission.authMethod, - permission.orgId - ); + await permissionService.getOrgPermission({ + actor: permission.type, + actorId: permission.id, + orgId: permission.orgId, + actorAuthMethod: permission.authMethod, + actorOrgId: permission.orgId, + scope: OrganizationActionScope.Any + }); const projectMember = await permissionService .getProjectPermission({ diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 74c7e95f4..18ae74350 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -41,7 +41,7 @@ export type TCreateProjectDTO = { actor: ActorType; actorAuthMethod: ActorAuthMethod; actorId: string; - actorOrgId?: string; + actorOrgId: string; projectName: string; projectDescription?: string; slug?: string; diff --git a/backend/src/services/role/org/org-role-factory.ts b/backend/src/services/role/org/org-role-factory.ts index 50ffa5e43..f91dabccb 100644 --- a/backend/src/services/role/org/org-role-factory.ts +++ b/backend/src/services/role/org/org-role-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { orgAdminPermissions, orgMemberPermissions, @@ -34,35 +34,38 @@ export const newOrgRoleFactory = ({ 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role); const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({ @@ -78,35 +81,38 @@ export const newOrgRoleFactory = ({ }; 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); 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 - ); + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role); }; diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts index 41c825b2e..3387dc96b 100644 --- a/backend/src/services/role/role-service.ts +++ b/backend/src/services/role/role-service.ts @@ -1,7 +1,7 @@ import { packRules } from "@casl/ability/extra"; import { requestContext } from "@fastify/request-context"; -import { AccessScope, ActionProjectType, TableName } from "@app/db/schemas"; +import { AccessScope, ActionProjectType, OrganizationActionScope, 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"; @@ -214,13 +214,14 @@ export const roleServiceFactory = ({ 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 - ); + const { permission, memberships } = await permissionService.getOrgPermission({ + actorId: dto.permission.id, + actor: dto.permission.type, + orgId: dto.permission.orgId, + actorOrgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + scope: OrganizationActionScope.Any + }); return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails: undefined }; } diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 4cbfcdc7f..87dd207f1 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -1,4 +1,4 @@ -import { TSecretSharing } from "@app/db/schemas"; +import { OrganizationActionScope, TSecretSharing } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; @@ -81,12 +81,21 @@ export const secretSharingServiceFactory = ({ }: TCreateSharedSecretDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId, + scope: OrganizationActionScope.Any + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); - const org = await orgDAL.findOrgById(orgId); - if (!org.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { + const rootOrg = await orgDAL.findRootOrgDetails(orgId); + if (!rootOrg) throw new BadRequestError({ message: `Organization with id ${orgId} not found` }); + + if (!rootOrg.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) { throw new BadRequestError({ message: "Organization does not allow sharing secrets to members outside of this organization" }); @@ -100,13 +109,16 @@ export const secretSharingServiceFactory = ({ const expiresAtTimestamp = new Date(expiresAt).getTime(); const lifetime = expiresAtTimestamp - new Date().getTime(); - // org.maxSharedSecretLifetime is in seconds - if (org.maxSharedSecretLifetime && lifetime / 1000 > org.maxSharedSecretLifetime) { + // rootOrg.maxSharedSecretLifetime is in seconds + if (rootOrg.maxSharedSecretLifetime && lifetime / 1000 > rootOrg.maxSharedSecretLifetime) { throw new BadRequestError({ message: "Secret lifetime exceeds organization limit" }); } // Check max view count is within org allowance - if (org.maxSharedSecretViewLimit && (!expiresAfterViews || expiresAfterViews > org.maxSharedSecretViewLimit)) { + if ( + rootOrg.maxSharedSecretViewLimit && + (!expiresAfterViews || expiresAfterViews > rootOrg.maxSharedSecretViewLimit) + ) { throw new BadRequestError({ message: "Secret max views parameter exceeds organization limit" }); } @@ -122,7 +134,10 @@ export const secretSharingServiceFactory = ({ if (allOrgMembers.some((v) => v.user.email === email)) { orgEmails.push(email); // If the email is not part of the org, but access type / org settings require it - } else if (!org.allowSecretSharingOutsideOrganization || accessType === SecretSharingAccessType.Organization) { + } else if ( + !rootOrg.allowSecretSharingOutsideOrganization || + accessType === SecretSharingAccessType.Organization + ) { throw new BadRequestError({ message: "Organization does not allow sharing secrets to members outside of this organization" }); @@ -196,7 +211,14 @@ export const secretSharingServiceFactory = ({ actorAuthMethod, actorOrgId }: TCreateSecretRequestDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); $validateSharedSecretExpiry(expiresAt); @@ -228,7 +250,14 @@ export const secretSharingServiceFactory = ({ throw new NotFoundError({ message: `Secret request with ID '${id}' not found` }); } - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) { @@ -267,13 +296,14 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - secretRequest.orgId, + orgId: secretRequest.orgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); } @@ -316,13 +346,14 @@ export const secretSharingServiceFactory = ({ throw new UnauthorizedError(); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - secretRequest.orgId, + orgId: secretRequest.orgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" }); const user = await userDAL.findById(actorId); @@ -415,13 +446,14 @@ export const secretSharingServiceFactory = ({ }: TGetSharedSecretsDTO) => { if (!actorOrgId) throw new ForbiddenRequestError(); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, actorOrgId - ); + }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const secrets = await secretSharingDAL.find( @@ -563,7 +595,14 @@ export const secretSharingServiceFactory = ({ const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => { const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput; - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission({ + scope: OrganizationActionScope.Any, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }); if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" }); const sharedSecret = isUuidV4(sharedSecretId) diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index 2aa495673..081b99208 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -14,6 +14,7 @@ import { logger } from "@app/lib/logger"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { ActorType } from "../auth/auth-type"; +import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; @@ -29,6 +30,7 @@ import { type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; userDAL: TUserDALFactory; + orgDAL: Pick; permissionService: Pick; projectEnvDAL: Pick; projectDAL: Pick; @@ -45,7 +47,8 @@ export const serviceTokenServiceFactory = ({ projectEnvDAL, projectDAL, accessTokenQueue, - smtpService + smtpService, + orgDAL }: TServiceTokenServiceFactoryDep) => { const createServiceToken = async ({ iv, @@ -184,7 +187,15 @@ export const serviceTokenServiceFactory = ({ if (!isMatch) throw new UnauthorizedError({ message: "Invalid service token" }); await accessTokenQueue.updateServiceTokenStatus(serviceToken.id); - return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; + const serviceTokenOrgDetails = await orgDAL.findById(project.orgId); + + return { + ...serviceToken, + lastUsed: new Date(), + orgId: project.orgId, + parentOrgId: serviceTokenOrgDetails.parentOrgId || serviceTokenOrgDetails.id, + rootOrgId: serviceTokenOrgDetails.rootOrgId || serviceTokenOrgDetails.id + }; }; const notifyExpiringTokens = async () => { diff --git a/backend/src/services/slack/slack-service.ts b/backend/src/services/slack/slack-service.ts index c8aa8aaf6..e4110ac11 100644 --- a/backend/src/services/slack/slack-service.ts +++ b/backend/src/services/slack/slack-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { InstallProvider } from "@slack/oauth"; +import { OrganizationActionScope } 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 { getConfig } from "@app/lib/config/env"; @@ -230,13 +231,14 @@ export const slackServiceFactory = ({ }: TGetSlackInstallUrlDTO) => { const appCfg = getConfig(); - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -264,13 +266,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -293,13 +296,14 @@ export const slackServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetSlackIntegrationByOrgDTO) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); @@ -324,13 +328,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -351,13 +356,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); @@ -389,13 +395,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); @@ -432,13 +439,14 @@ export const slackServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - slackIntegration.orgId, + orgId: slackIntegration.orgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); diff --git a/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx new file mode 100644 index 000000000..da93fc045 --- /dev/null +++ b/backend/src/services/smtp/emails/SubOrganizationInvitationTemplate.tsx @@ -0,0 +1,50 @@ +import { Heading, Section, Text } from "@react-email/components"; +import React from "react"; + +import { BaseButton } from "./BaseButton"; +import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; + +interface SubOrganizationInvitationTemplateProps extends Omit { + callback_url: string; + subOrganizationName: string; +} + +export const SubOrganizationInvitationTemplate = ({ + callback_url, + subOrganizationName, + siteUrl +}: SubOrganizationInvitationTemplateProps) => { + return ( + + + You've been invited to join a sub-organization on Infisical + +
+ + You've been invited to join the sub-organization {subOrganizationName}. + +
+
+ Join Sub-Organization +
+
+ + About Infisical: Infisical is an all-in-one platform to securely manage application secrets, + certificates, SSH keys, and configurations across your team and infrastructure. + +
+
+ ); +}; + +export default SubOrganizationInvitationTemplate; + +SubOrganizationInvitationTemplate.PreviewProps = { + subOrganizationName: "Example Project", + siteUrl: "https://infisical.com", + callback_url: "https://app.infisical.com" +} as SubOrganizationInvitationTemplateProps; diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 06ac31ab6..692cacbaf 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -31,4 +31,5 @@ export * from "./SecretScanningSecretsDetectedTemplate"; export * from "./SecretSyncFailedTemplate"; export * from "./ServiceTokenExpiryNoticeTemplate"; export * from "./SignupEmailVerificationTemplate"; +export * from "./SubOrganizationInvitationTemplate"; export * from "./UnlockAccountTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 652f56567..cef22009a 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -40,6 +40,7 @@ import { SecretSyncFailedTemplate, ServiceTokenExpiryNoticeTemplate, SignupEmailVerificationTemplate, + SubOrganizationInvitationTemplate, UnlockAccountTemplate } from "./emails"; @@ -65,6 +66,7 @@ export enum SmtpTemplates { // HistoricalSecretList = "historicalSecretLeakIncident", not used anymore? NewDeviceJoin = "newDevice", OrgInvite = "organizationInvitation", + SubOrgInvite = "subOrganizationInvitation", OrgAssignment = "organizationAssignment", OAuthPasswordReset = "oAuthPasswordReset", ResetPassword = "passwordReset", @@ -102,6 +104,7 @@ export enum SmtpHost { // eslint-disable-next-line @typescript-eslint/no-explicit-any const EmailTemplateMap: Record> = { [SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate, + [SmtpTemplates.SubOrgInvite]: SubOrganizationInvitationTemplate, [SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate, [SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate, [SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate, diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 84a53f407..63bab2666 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -592,7 +592,7 @@ export const superAdminServiceFactory = ({ }); const { identity, credentials } = await identityDAL.transaction(async (tx) => { - const newIdentity = await identityDAL.create({ name: "Instance Admin Identity" }, tx); + const newIdentity = await identityDAL.create({ name: "Instance Admin Identity", orgId: organization.id }, tx); const membership = await membershipIdentityDAL.create( { actorIdentityId: newIdentity.id, diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b54eab8ef..56d7ee635 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { Knex } from "knex"; -import { AccessScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope } 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"; @@ -458,13 +458,14 @@ export const userServiceFactory = ({ // This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission. if (user.id !== actorId) { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); } diff --git a/backend/src/services/workflow-integration/workflow-integration-service.ts b/backend/src/services/workflow-integration/workflow-integration-service.ts index cb7f7a325..8fea0e240 100644 --- a/backend/src/services/workflow-integration/workflow-integration-service.ts +++ b/backend/src/services/workflow-integration/workflow-integration-service.ts @@ -1,5 +1,6 @@ import { ForbiddenError } from "@casl/ability"; +import { OrganizationActionScope } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -23,13 +24,14 @@ export const workflowIntegrationServiceFactory = ({ actorOrgId, actorAuthMethod }: TGetWorkflowIntegrationsByOrg) => { - const { permission } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission({ actor, actorId, - actorOrgId, + orgId: actorOrgId, actorAuthMethod, - actorOrgId - ); + actorOrgId, + scope: OrganizationActionScope.Any + }); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 3c9e743ff..e3f72f61b 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -24,7 +24,7 @@ const SCOPE_NAME: Record, { label: string; icon: Ico [ProjectType.KMS]: { label: "Project", icon: faCube }, [ProjectType.PAM]: { label: "Project", icon: faCube }, [ProjectType.SecretScanning]: { label: "Project", icon: faCube }, - namespace: { label: "Namespace", icon: faCubes }, + namespace: { label: "Sub-Organization", icon: faCubes }, instance: { label: "Server", icon: faServer } }; diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index a37b38cb6..16b8b4f45 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -24,6 +24,8 @@ apiRequest.interceptors.request.use((config) => { const token = getAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken(); + const params = new URLSearchParams(window.location.search); + if (config.headers) { if (signupTempToken) { // eslint-disable-next-line no-param-reassign @@ -38,6 +40,17 @@ apiRequest.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${providerAuthToken}`; } + + const rootOrgHeader = config.headers.get("x-root-org"); + + if (rootOrgHeader) { + config.headers.delete("x-root-org"); + } else { + const subOrganization = params.get("subOrganization"); + if (subOrganization) { + config.headers.set("x-infisical-org", subOrganization); + } + } } return config; diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 2e835a1db..410df7440 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -33,10 +33,6 @@ export const ROUTE_PATHS = Object.freeze({ "/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" ), - SecretSharingSettings: setRoute( - "/organization/secret-sharing/settings", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" - ), SettingsPage: setRoute( "/organization/settings", "/_authenticate/_inject-org-details/_org-layout/organization/settings/" diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index bcab6169e..87dc40263 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -62,7 +62,8 @@ export enum OrgPermissionSubjects { SecretShare = "secret-share", GithubOrgSync = "github-org-sync", GithubOrgSyncManual = "github-org-sync-manual", - MachineIdentityAuthTemplate = "machine-identity-auth-template" + MachineIdentityAuthTemplate = "machine-identity-auth-template", + SubOrganization = "sub-organization" } export enum OrgPermissionAdminConsoleAction { @@ -112,6 +113,11 @@ export enum OrgPermissionGroupActions { RemoveMembers = "remove-members" } +export enum OrgPermissionSubOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -151,6 +157,7 @@ export type OrgPermissionSet = | OrgPermissionSubjects.AppConnections | (ForcedSubject & AppConnectionSubjectFields) ) - ]; + ] + | [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index 26865b5bf..bfc17d143 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,5 +1,6 @@ +import { useMemo } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { useRouteContext } from "@tanstack/react-router"; +import { useRouteContext, useSearch } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; @@ -9,11 +10,29 @@ export const useOrganization = () => { select: (el) => el.organizationId }); + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); + const { data: currentOrg } = useSuspenseQuery({ - queryKey: organizationKeys.getOrgById(organizationId), + queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"), queryFn: () => fetchOrganizationById(organizationId), staleTime: Infinity }); - return { currentOrg }; + const org = useMemo( + () => ({ + currentOrg: { + ...currentOrg, + id: currentOrg?.subOrganization?.id || currentOrg?.id, + parentOrgId: currentOrg.id + }, + isSubOrganization: Boolean(currentOrg.subOrganization), + isRootOrganization: !currentOrg.subOrganization + }), + [currentOrg, subOrganization] + ); + + return org; }; diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx index 529a74930..762b067c2 100644 --- a/frontend/src/hoc/withPermission/withPermission.tsx +++ b/frontend/src/hoc/withPermission/withPermission.tsx @@ -24,7 +24,7 @@ export const withPermission = ( // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied - if (permission.cannot(action as any, subject)) { + if (permission.cannot(action as any, subject as any)) { return (
{ + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => { + const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>( + `/api/v1/organization/identity-memberships/${identityId}`, + { roles } + ); + return data.identityMembership; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) }); + } + }); +}; + +export const useDeleteOrgIdentityMembership = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => { + const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>( + `/api/v1/organization/identity-memberships/${identityId}` + ); + return data.identityMembership; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: identitiesKeys.searchIdentities({ search: {} }) }); + } + }); +}; diff --git a/frontend/src/hooks/api/orgIdentityMembership/types.ts b/frontend/src/hooks/api/orgIdentityMembership/types.ts new file mode 100644 index 000000000..95fa06b82 --- /dev/null +++ b/frontend/src/hooks/api/orgIdentityMembership/types.ts @@ -0,0 +1,32 @@ +export enum TemporaryPermissionMode { + Relative = "relative" +} + +export type TOrgIdentityMembership = { + id: string; + orgId: string; + identityId: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreateOrgIdentityMembershipDTO = { + identityId: string; + roles: Array< + | { + role: string; + isTemporary?: false; + } + | { + role: string; + isTemporary: true; + temporaryMode: TemporaryPermissionMode; + temporaryRange: string; + temporaryAccessStartTime: string; + } + >; +}; + +export type TDeleteOrgIdentityMembershipDTO = { + identityId: string; +}; diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index f4627a614..7c283691e 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -6,6 +6,7 @@ export { useDeleteOrgById, useDeleteOrgPmtMethod, useDeleteOrgTaxId, + useGetAvailableOrgIdentities, useGetIdentityMembershipOrgs, useGetOrganizationGroups, useGetOrganizations, diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 15e1b861c..bbf73dd25 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -42,7 +42,9 @@ export const organizationKeys = { [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, - getOrgById: (orgId: string) => ["organization", { orgId }] + getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }], + getAvailableIdentities: () => ["available-identities"], + getAvailableUsers: () => ["available-users"] }; export const fetchOrganizations = async () => { @@ -64,7 +66,9 @@ export const useGetOrganizations = () => { export const fetchOrganizationById = async (id: string) => { const { data: { organization } - } = await apiRequest.get<{ organization: Organization }>(`/api/v1/organization/${id}`); + } = await apiRequest.get<{ + organization: Organization & { subOrganization?: { id: string; name: string } }; + }>(`/api/v1/organization/${id}`); return organization; }; @@ -572,3 +576,29 @@ export const useGetOrgIntegrationAuths = ( select }); }; + +export const useGetAvailableOrgIdentities = (enabled = true) => + useQuery({ + queryKey: organizationKeys.getAvailableIdentities(), + queryFn: async () => { + const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>( + "/api/v1/organization/identities/available" + ); + + return data.identities; + }, + enabled + }); + +export const useGetAvailableOrgUsers = (enabled = true) => + useQuery({ + queryKey: organizationKeys.getAvailableUsers(), + queryFn: async () => { + const { data } = await apiRequest.get<{ + users: { username: string; id: string; firstName: string; lastName: string }[]; + }>("/api/v1/organization/users/available"); + + return data.users; + }, + enabled + }); diff --git a/frontend/src/hooks/api/subOrganizations/index.tsx b/frontend/src/hooks/api/subOrganizations/index.tsx new file mode 100644 index 000000000..480377464 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/index.tsx @@ -0,0 +1,8 @@ +export { useCreateSubOrganization, useUpdateSubOrganization } from "./mutations"; +export { subOrganizationsQuery } from "./queries"; +export type { + TCreateSubOrganizationDTO, + TListSubOrganizationsDTO, + TSubOrganization, + TUpdateSubOrganizationDTO +} from "./types"; diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx new file mode 100644 index 000000000..81369a62e --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { subOrganizationsQuery } from "./queries"; +import { TCreateSubOrganizationDTO, TSubOrganization, TUpdateSubOrganizationDTO } from "./types"; + +export const useCreateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (dto: TCreateSubOrganizationDTO) => { + const { data } = await apiRequest.post<{ organization: TSubOrganization }>( + "/api/v1/sub-organizations", + dto, + { + headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header + } + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; + +export const useUpdateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subOrgId, name }: TUpdateSubOrganizationDTO) => { + const { data } = await apiRequest.patch<{ organization: TSubOrganization }>( + `/api/v1/sub-organizations/${subOrgId}`, + { name } + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/subOrganizations/queries.tsx b/frontend/src/hooks/api/subOrganizations/queries.tsx new file mode 100644 index 000000000..99ccb7770 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/queries.tsx @@ -0,0 +1,28 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TListSubOrganizationsDTO, TSubOrganization } from "./types"; + +export const subOrganizationsQuery = { + allKey: () => ["sub-organizations"] as const, + listKey: (params?: TListSubOrganizationsDTO) => + [...subOrganizationsQuery.allKey(), "list", params] as const, + list: (params: TListSubOrganizationsDTO) => + queryOptions({ + queryKey: subOrganizationsQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ organizations: TSubOrganization[] }>( + "/api/v1/sub-organizations", + { + params: { + limit: params.limit, + offset: params.offset, + isAccessible: params.isAccessible + } + } + ); + return data.organizations; + } + }) +}; diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts new file mode 100644 index 000000000..e9b3f2f01 --- /dev/null +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -0,0 +1,23 @@ +export type TSubOrganization = { + id: string; + name: string; + slug: string; + createdAt: string; + updatedAt: string; + parentOrgId: string; +}; + +export type TCreateSubOrganizationDTO = { + name: string; +}; + +export type TListSubOrganizationsDTO = { + limit?: number; + offset?: number; + isAccessible?: boolean; +}; + +export type TUpdateSubOrganizationDTO = { + subOrgId: string; + name: string; +}; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 5c0fa687b..98daf3ec1 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -13,6 +13,7 @@ export type SubscriptionPlan = { customRateLimits: boolean; pitRecovery: boolean; githubOrgSync: boolean; + subOrganization?: boolean; ipAllowlisting: boolean; rbac: boolean; secretVersioning: boolean; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index f3182ab83..f9ef51133 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -6,12 +6,15 @@ import { faBook, faCaretDown, faCheck, + faChevronRight, + faCubes, faEnvelope, faExclamationTriangle, faGlobe, faInfinity, faInfo, faInfoCircle, + faPlus, faServer, faSignOut, faToolbox, @@ -19,7 +22,7 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; @@ -34,6 +37,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + DropdownSubMenu, + DropdownSubMenuContent, + DropdownSubMenuTrigger, IconButton, Modal, ModalContent, @@ -44,15 +50,22 @@ import { envConfig } from "@app/config/env"; import { useOrganization, useSubscription, useUser } from "@app/context"; import { isInfisicalCloud } from "@app/helpers/platform"; import { useToggle } from "@app/hooks"; -import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; +import { + projectKeys, + subOrganizationsQuery, + useGetOrganizations, + useGetOrgTrialUrl, + useLogoutUser +} from "@app/hooks/api"; import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { getAuthToken } from "@app/hooks/api/reactQuery"; -import { SubscriptionPlan } from "@app/hooks/api/types"; +import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { AuthMethod } from "@app/hooks/api/users/types"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; +import { NewSubOrganizationForm } from "./NewSubOrganizationForm"; import { NotificationDropdown } from "./NotificationDropdown"; const getPlan = (subscription: SubscriptionPlan) => { @@ -119,10 +132,18 @@ export const INFISICAL_SUPPORT_OPTIONS = [ export const Navbar = () => { const { user } = useUser(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); + const [showAdminsModal, setShowAdminsModal] = useState(false); + const [showSubOrgForm, setShowSubOrgForm] = useState(false); const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true }); + const { data: subOrganizations = [] } = useQuery({ + ...subOrgQuery, + enabled: Boolean(subscription.subOrganization) + }); + useEffect(() => { if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) { sessionStorage.setItem("paymentFailed", "true"); @@ -137,6 +158,7 @@ export const Navbar = () => { const [shouldShowMfa, toggleShowMfa] = useToggle(false); const router = useRouter(); const queryClient = useQueryClient(); + const [isOrgSelectOpen, setIsOrgSelectOpen] = useState(false); const location = useLocation(); const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context }); @@ -161,6 +183,7 @@ export const Navbar = () => { } await router.invalidate(); await navigateUserToOrg(navigate, orgId); + queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); }; const { mutateAsync } = useGetOrgTrialUrl(); @@ -206,6 +229,33 @@ export const Navbar = () => { const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate? + const handleOrgNav = async (org: Organization) => { + if (currentOrg?.id === org.id) return; + + if (org.authEnforced) { + // org has an org-level auth method enabled (e.g. SAML) + // -> logout + redirect to SAML SSO + + await logout.mutateAsync(); + if (org.orgAuthMethod === AuthMethod.OIDC) { + window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`); + } else { + window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`); + } + window.close(); + return; + } + + if (org.googleSsoAuthEnforced) { + await logout.mutateAsync(); + window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`); + window.close(); + return; + } + + handleOrgChange(org?.id); + }; + return (
@@ -235,38 +285,45 @@ export const Navbar = () => { ) : ( <>
- - -
- - -

{currentOrg?.name}

-
-
- {getPlan(subscription)} -
- {subscription.cardDeclined && ( - -
- -
-
+ +
+ { + navigate({ + to: "/organization/projects", + search: (search) => ({ ...search, subOrganization: undefined }) + }); + if (isSubOrganization) { + await router.invalidate({ sync: true }).catch(() => null); + } + }} + variant="org" + className={twMerge( + "max-w-full min-w-0 cursor-pointer text-sm", + (!isOrgScope || isSubOrganization) && + "bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline" )} + > + +

{currentOrg?.name}

+
+
+ {getPlan(subscription)}
- + {subscription.cardDeclined && ( + +
+ +
+
+ )} +
{
- organizations + Organizations
{orgs?.map((org) => { + if ( + subscription.subOrganization && + (org.id === currentOrg?.id || org.id === currentOrg?.parentOrgId) + ) { + return ( + + { + setIsOrgSelectOpen(false); + handleOrgNav(org); + }} + className="cursor-pointer font-normal" + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+ +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + ); + } + return ( - - + handleOrgNav(org)} + className="cursor-pointer font-normal" + key={org.id} + > +
+ {currentOrg?.id === org.id && ( + + )} +

{org.name}

+
); })} @@ -345,6 +439,78 @@ export const Navbar = () => {
+ {currentOrg.subOrganization && ( + <> +

/

+ + + + +

{currentOrg.subOrganization.name}

+
+ + +
+ + + +
+
+ +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + { + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: subOrg.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); + }} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + + + )} {!isOrgScope && ( <>

/

@@ -517,6 +683,7 @@ export const Navbar = () => { + {
+ + +
+ { + setShowSubOrgForm(false); + }} + /> +
+
+
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx new file mode 100644 index 000000000..75041a69e --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -0,0 +1,94 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate, useRouter } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useCreateSubOrganization } from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +type ContentProps = { + onClose: () => void; +}; + +const AddOrgSchema = z.object({ + name: slugSchema() +}); + +type FormData = z.infer; + +export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { + const createSubOrg = useCreateSubOrganization(); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ + defaultValues: { + name: "" + }, + resolver: zodResolver(AddOrgSchema) + }); + + const navigate = useNavigate(); + const router = useRouter(); + + const onSubmit = async ({ name }: FormData) => { + try { + const { organization } = await createSubOrg.mutateAsync({ + name + }); + + createNotification({ + type: "success", + text: "Successfully created sub organization" + }); + onClose(); + + navigate({ + to: "/organization/projects", + search: (prev) => ({ ...prev, subOrganization: organization.name }) + }); + await router.invalidate({ sync: true }).catch(() => null); + } catch { + createNotification({ + text: "Failed to create sub organization", + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + control={control} + name="name" + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index ba34877f9..564970904 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -3,6 +3,7 @@ import { motion } from "framer-motion"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Tab, TabList, Tabs } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { usePopUp } from "@app/hooks"; type Props = { @@ -10,10 +11,13 @@ type Props = { }; export const OrgNavBar = ({ isHidden }: Props) => { + const { isRootOrganization } = useOrganization(); const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const); const { pathname } = useLocation(); + const variant = isRootOrganization ? "org" : "namespace"; + return ( <> {!isHidden && ( @@ -30,28 +34,28 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( - + Overview )} {({ isActive }) => ( - + App Connections )} {({ isActive }) => ( - + Networking )} {({ isActive }) => ( - + Secret Sharing )} @@ -59,7 +63,7 @@ export const OrgNavBar = ({ isHidden }: Props) => { {({ isActive }) => ( { {({ isActive }) => ( - + Audit Logs )} - - {({ isActive }) => ( - - Usage & Billing - - )} - + {isRootOrganization && ( + + {({ isActive }) => ( + + Usage & Billing + + )} + + )} {({ isActive }) => ( - + Settings )} diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index 4e26a7393..72430fec6 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -171,9 +171,21 @@ export const ProjectSelect = () => { to: getProjectHomePage(workspace.type, workspace.environments), params: { projectId: workspace.id + }, + search: { + subOrganization: currentOrg?.subOrganization?.name } }); - window.location.assign(url.to.replaceAll("$projectId", workspace.id)); + const urlInstance = new URL( + `${window.location.origin}/${url.to.replaceAll("$projectId", workspace.id)}` + ); + if (currentOrg?.subOrganization) { + urlInstance.searchParams.set( + "subOrganization", + currentOrg.subOrganization.name + ); + } + window.location.assign(urlInstance); }} icon={ currentWorkspace?.id === workspace.id && ( diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 8059f71ce..d3e93bcea 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from " export const AccessManagementPage = () => { const { t } = useTranslation(); const { permission } = useOrgPermission(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const navigate = useNavigate({ from: ROUTE_PATHS.Organization.AccessControlPage.path @@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => { {tabSections .filter((el) => !el.isHidden) .map((el) => ( - + {el.label} ))} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx new file mode 100644 index 000000000..b0977437b --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLinkForm.tsx @@ -0,0 +1,129 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FilterableSelect, FormControl } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api"; +import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership"; + +const schema = z + .object({ + identity: z.object({ name: z.string(), id: z.string() }), + role: z.object({ name: z.string(), slug: z.string() }) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + onClose: () => void; +}; + +export const IdentityLinkForm = ({ onClose }: Props) => { + const navigate = useNavigate(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + + const { data: roles } = useGetOrgRoles(orgId); + + const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership(); + const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities(); + + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: {} + }); + + const onFormSubmit = async ({ identity, role }: FormData) => { + try { + await createMutateAsync({ + identityId: identity.id, + roles: [{ role: role.slug, isTemporary: false }] + }); + createNotification({ + text: "Successfully linked identity", + type: "success" + }); + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: identity.id + } + }); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to link identity"; + + createNotification({ + text, + type: "error" + }); + } + }; + + return ( +
+ ( + + option.id} + getOptionLabel={(option) => option.name} + isLoading={isRootOrgLoading} + /> + + )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + // menuPortalTarget={document.body} + /> + + )} + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 224af9d5e..dacbba428 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -53,6 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { const orgId = currentOrg?.id || ""; const { data: roles } = useGetOrgRoles(orgId); + const isOrgIdentity = popUp?.identity?.data ? orgId === popUp?.identity?.data?.orgId : true; const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); @@ -113,6 +114,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { name: string; role: string; hasDeleteProtection: boolean; + orgId: string; }; if (identity) { @@ -196,16 +198,23 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`} >
- ( - - - - )} - /> + {isOrgIdentity && ( + ( + + + + )} + /> + )} { label={`${popUp?.identity?.data ? "Update" : ""} Role`} errorText={error?.message} isError={Boolean(error)} - className="mt-4" > { )} /> - ( - - -

Delete Protection {value ? "Enabled" : "Disabled"}

-
-
- )} - /> + {isOrgIdentity && ( + ( + + +

Delete Protection {value ? "Enabled" : "Disabled"}

+
+
+ )} + /> + )}
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 9280ab5d9..5c15a0cdd 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -1,10 +1,15 @@ -import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowUpRightFromSquare, + faBookOpen, + faLink, + faPlus +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal } from "@app/components/v2"; +import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, @@ -19,6 +24,7 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal"; import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable"; +import { IdentityLinkForm } from "./IdentityLinkForm"; import { IdentityModal } from "./IdentityModal"; import { IdentityTable } from "./IdentityTable"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; @@ -27,7 +33,7 @@ import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal export const IdentitySection = withPermission( () => { const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); @@ -43,7 +49,8 @@ export const IdentitySection = withPermission( "createTemplate", "editTemplate", "deleteTemplate", - "viewUsages" + "viewUsages", + "linkIdentity" ] as const); const isMoreIdentitiesAllowed = subscription?.identityLimit @@ -105,8 +112,8 @@ export const IdentitySection = withPermission( return (
-
-
+
+

Identities

+ {isSubOrganization && ( + + {(isAllowed) => ( + + )} + + )} - {/* */} - {/* */} + handlePopUpToggle("linkIdentity", isOpen)} + > + + handlePopUpClose("linkIdentity")} /> + + { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { offset, @@ -286,15 +287,18 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
+ {isSubOrganization && Managed By} {isFetching ? : null} - {isPending && } + {isPending && ( + + )} {!isPending && data?.identities?.map( ({ - identity: { id, name }, + identity: { id, name, orgId }, role, customRole, lastLoginAuthMethod, @@ -362,6 +366,14 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { }} + {isSubOrganization && ( + +

+ + {currentOrg.id === orgId ? "Sub Organization" : "Root Organization"} +

+ + )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx new file mode 100644 index 000000000..290f8db4b --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddSubOrgMemberModal.tsx @@ -0,0 +1,280 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { RoleOption } from "@app/components/roles"; +import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { findOrgMembershipRole } from "@app/helpers/roles"; +import { + useAddUsersToOrg, + useAddUserToWsNonE2EE, + useGetOrgRoles, + useGetUserProjects +} from "@app/hooks/api"; +import { useGetAvailableOrgUsers } from "@app/hooks/api/organization/queries"; +import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; + +const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member"; + +const addMemberFormSchema = z.object({ + users: z + .array( + z.object({ + username: z.string().trim(), + email: z.string().trim() + }) + ) + .min(1), + projects: z + .array( + z.object({ + name: z.string(), + id: z.string(), + slug: z.string(), + version: z.nativeEnum(ProjectVersion) + }) + ) + .default([]), + projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG), + organizationRole: z.object({ + name: z.string(), + slug: z.string(), + description: z.string().optional() + }) +}); + +type TAddMemberForm = z.infer; + +type Props = { + onClose: () => void; +}; + +export const AddSubOrgMemberModal = ({ onClose }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); + const { data: members = [], isPending: isMembersPending } = useGetAvailableOrgUsers(); + + const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); + const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE(); + + const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({ + includeRoles: true + }); + + const { + control, + handleSubmit, + watch, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(addMemberFormSchema) + }); + + // set initial form role based off org default role + useEffect(() => { + if (organizationRoles) { + reset({ + organizationRole: findOrgMembershipRole(organizationRoles, currentOrg.defaultMembershipRole) + }); + } + }, [organizationRoles]); + + const onAddMembers = async ({ + users, + organizationRole, + projects: selectedProjects, + projectRoleSlug + }: TAddMemberForm) => { + if (!currentOrg?.id) return; + + if (selectedProjects?.length) { + // eslint-disable-next-line no-restricted-syntax + for (const project of selectedProjects) { + if (project.version !== ProjectVersion.V3) { + createNotification({ + type: "error", + text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.` + }); + return; + } + } + } + + try { + const usernames = users.map((el) => el.username); + await addUsersMutateAsync({ + organizationId: currentOrg?.id, + inviteeEmails: usernames, + organizationRoleSlug: organizationRole.slug + }); + + await Promise.allSettled( + selectedProjects.map((el) => + addUserToProject({ + orgId: currentOrg.id, + projectId: el.id, + roleSlugs: [projectRoleSlug], + usernames + }) + ) + ); + onClose(); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to add user to suborganization", + type: "error" + }); + } + }; + + const getGroupHeaderLabel = (type: ProjectType) => { + switch (type) { + case ProjectType.SecretManager: + return "Secrets"; + case ProjectType.CertificateManager: + return "PKI"; + case ProjectType.KMS: + return "KMS"; + case ProjectType.SSH: + return "SSH"; + default: + return "Other"; + } + }; + + return ( + + ( + + option.username} + getOptionLabel={(option) => option.username} + /* eslint-disable-next-line react/no-unstable-nested-components */ + noOptionsMessage={() => ( +

All root organization users are already assigned to this project

+ )} + /> +
+ )} + /> + ( + + option.slug} + getOptionLabel={(option) => option.name} + value={value} + onChange={onChange} + components={{ Option: RoleOption }} + /> + + )} + /> + +
+
+ ( + + project.name} + getOptionValue={(project) => project.id} + options={projects} + groupBy="type" + getGroupHeaderLabel={getGroupHeaderLabel} + placeholder="Select projects..." + /> + + )} + /> +
+
+ ( + +
+ +
+
+ )} + /> +
+
+ +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 010a12b38..a8f5ea059 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -11,6 +11,8 @@ import { Button, DeleteActionModal, EmailServiceSetupModal, + Modal, + ModalContent, Tooltip } from "@app/components/v2"; import { @@ -26,11 +28,12 @@ import { OrgUser } from "@app/hooks/api/users/types"; import { usePopUp } from "@app/hooks/usePopUp"; import { AddOrgMemberModal } from "./AddOrgMemberModal"; +import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal"; import { OrgMembersTable } from "./OrgMembersTable"; export const OrgMembersSection = () => { const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id ?? ""; const { user } = useUser(); const userId = user?.id || ""; @@ -41,6 +44,7 @@ export const OrgMembersSection = () => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addMember", + "addMemberToSubOrg", "removeMember", "deactivateMember", "upgradePlan", @@ -210,7 +214,9 @@ export const OrgMembersSection = () => { colorSchema="secondary" type="submit" leftIcon={} - onClick={() => handleAddMemberModal()} + onClick={() => + isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal() + } isDisabled={!isAllowed} > Add Member @@ -230,6 +236,14 @@ export const OrgMembersSection = () => { completeInviteLinks={completeInviteLinks} setCompleteInviteLinks={setCompleteInviteLinks} /> + handlePopUpToggle("addMemberToSubOrg", isOpen)} + > + + handlePopUpClose("addMemberToSubOrg")} /> + + { const navigate = useNavigate(); const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { user } = useUser(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -586,6 +586,7 @@ export const OrgMembersTable = ({ {isActive && (status === "invited" || status === "verified") && email && + !isSubOrganization && serverDetails?.emailConfigured && ( { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -200,7 +200,9 @@ export const OrgRoleTable = () => { return (
-

Organization Roles

+

+ {isSubOrganization ? "Sub-" : ""}Organization Roles +

{(isAllowed) => (
{ const { hasOrgRole } = useOrgPermission(); + const { isSubOrganization } = useOrganization(); return (
- - + {isSubOrganization ? : } + {!isSubOrganization && } {hasOrgRole(OrgMembershipRole.Admin) && }
); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx index 9b6323c2e..766a22158 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/OrgNameChangeSection.tsx @@ -41,21 +41,19 @@ export const OrgNameChangeSection = (): JSX.Element => { const [isFormInitialized, setIsFormInitialized] = useState(false); useEffect(() => { - if (currentOrg) { - reset({ - name: currentOrg.name, - slug: currentOrg.slug, - ...(canReadOrgRoles && - roles?.length && { - // will always be present, can't remove role if default - defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole) - ? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || "" - : currentOrg.defaultMembershipRole - }) - }); - setIsFormInitialized(true); - } - }, [currentOrg, roles]); + reset({ + name: currentOrg.name, + slug: currentOrg.slug, + ...(canReadOrgRoles && + roles?.length && { + // will always be present, can't remove role if default + defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole) + ? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || "" + : currentOrg.defaultMembershipRole + }) + }); + setIsFormInitialized(true); + }, [roles]); const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => { try { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx new file mode 100644 index 000000000..ca625be6c --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -0,0 +1,101 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useRouter } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { useUpdateSubOrganization } from "@app/hooks/api"; + +const formSchema = z.object({ + name: z + .string() + .regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") +}); + +type FormData = z.infer; + +export const SubOrgNameChangeSection = (): JSX.Element => { + const { currentOrg } = useOrganization(); + const { permission } = useOrgPermission(); + const navigate = useNavigate(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: currentOrg?.subOrganization?.name || "" + } + }); + const { mutateAsync, isPending } = useUpdateSubOrganization(); + + const onFormSubmit = async ({ name }: FormData) => { + try { + await mutateAsync({ + name, + subOrgId: currentOrg.id + }); + + navigate({ to: "/organization/settings", search: { subOrganization: name } }); + queryClient.invalidateQueries(); + await router.invalidate({ sync: true }); + createNotification({ + text: "Successfully updated sub-organization details", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to update sub-organization details", + type: "error" + }); + } + }; + + return ( +
+
+

Organization Name

+ ( + + + + )} + control={control} + name="name" + /> +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx index 4d86fcddb..70fe29455 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx @@ -1 +1,2 @@ export { OrgNameChangeSection } from "./OrgNameChangeSection"; +export { SubOrgNameChangeSection } from "./SubOrgNameChangeSection"; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx index 61ee0c6e9..5be15edad 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx @@ -50,7 +50,7 @@ export const OrgProductSelectSection = () => { })); } }); - }, [currentOrg]); + }, [currentOrg?.id]); const onProductToggle = async (value: boolean, key: string) => { setIsLoading(true); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx index 66af7df1b..10eb0f6c0 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgTabGroup/OrgTabGroup.tsx @@ -3,6 +3,7 @@ import { useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; +import { useOrganization } from "@app/context"; import { AuditLogStreamsTab } from "../AuditLogStreamTab"; import { ExternalMigrationsTab } from "../ExternalMigrationsTab"; @@ -19,20 +20,33 @@ export const OrgTabGroup = () => { const search = useSearch({ from: ROUTE_PATHS.Organization.SettingsPage.id }); + const { isSubOrganization } = useOrganization(); + const tabs = [ { name: "General", key: "tab-org-general", component: OrgGeneralTab }, { name: "SSO", key: "sso-settings", - component: OrgSsoTab + component: OrgSsoTab, + isHidden: isSubOrganization }, { name: "Provisioning", key: "provisioning-settings", - component: OrgProvisioningTab + component: OrgProvisioningTab, + isHidden: isSubOrganization + }, + { + name: "Security", + key: "tab-org-security", + component: OrgSecurityTab, + isHidden: isSubOrganization + }, + { + name: "Encryption", + key: "tab-org-encryption", + component: OrgEncryptionTab }, - { name: "Security", key: "tab-org-security", component: OrgSecurityTab }, - { name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab }, { name: "Workflow Integrations", key: "workflow-integrations", @@ -57,17 +71,21 @@ export const OrgTabGroup = () => { return ( - {tabs.map((tab) => ( - - {tab.name} - - ))} + {tabs + .filter((el) => !el.isHidden) + .map((tab) => ( + + {tab.name} + + ))} - {tabs.map(({ key, component: Component }) => ( - - - - ))} + {tabs + .filter((el) => !el.isHidden) + .map(({ key, component: Component }) => ( + + + + ))} ); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx index 85580da07..d65f7d785 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx @@ -1,6 +1,5 @@ import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -70,154 +69,129 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTempla return (
- - {popUp?.editRole.isOpen ? ( - - handlePopUpClose("editRole")} - projectTemplate={projectTemplate} - role={editRole} - isDisabled={ - permission.cannot( - OrgPermissionActions.Edit, - OrgPermissionSubjects.ProjectTemplates - ) || - (editRole && !isCustomProjectRole(editRole.slug)) - } - /> -
- - ) : ( - -
-
-
-

Project Roles

-

- {isInfisicalTemplate - ? "Click a role to view the associated permissions" - : "Add, edit and remove roles for this project template"} -

-
- {!isInfisicalTemplate && ( - handlePopUpClose("editRole")} + projectTemplate={projectTemplate} + role={editRole} + isDisabled={ + permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates) || + (editRole && !isCustomProjectRole(editRole.slug)) + } + /> + ) : ( +
+
+
+

Project Roles

+

+ {isInfisicalTemplate + ? "Click a role to view the associated permissions" + : "Add, edit and remove roles for this project template"} +

+
+ {!isInfisicalTemplate && ( + + {(isAllowed) => ( + - )} - + Add Role + )} -
-
- - - - - - - - - - {roles.length ? ( - roles.map((role) => { - return ( - { - if (evt.key === "Enter") { - handlePopUpOpen("editRole", role); - } - }} - onClick={() => handlePopUpOpen("editRole", role)} - > - - - + + + )} + +
NameSlug -
{role.name}{role.slug} - {isCustomProjectRole(role.slug) && ( -
- + )} +
+
+ + + + + + + + + + {roles.length ? ( + roles.map((role) => { + return ( + { + if (evt.key === "Enter") { + handlePopUpOpen("editRole", role); + } + }} + onClick={() => handlePopUpOpen("editRole", role)} + > + + + - - ); - }) - ) : ( - - - )} - -
NameSlug +
{role.name}{role.slug} + {isCustomProjectRole(role.slug) && ( +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + e.preventDefault(); + handlePopUpOpen("removeRole", role); + }} > - {(isAllowed) => ( - { - e.stopPropagation(); - e.preventDefault(); - handlePopUpOpen("removeRole", role); - }} - > - - - )} - -
- )} -
- + + + )} + + + )}
-
-
- handlePopUpToggle("removeRole", isOpen)} - onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)} - /> - -
- - )} - + ); + }) + ) : ( +
+ +
+
+
+ handlePopUpToggle("removeRole", isOpen)} + onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)} + /> +
+ )}
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx index c542410bd..8d524c41c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/ProjectTemplatesSection.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -25,93 +24,70 @@ export const ProjectTemplatesSection = () => { return (
- - {editTemplate ? ( - - setEditTemplate(null)} - /> - - ) : ( - -
-

- Create and configure templates with predefined roles and environments to streamline - project setup -

-
-
-

Project Templates

- -
- - Docs - -
-
- - {(isAllowed) => ( - - )} - + {editTemplate ? ( + setEditTemplate(null)} /> + ) : ( +
+

+ Create and configure templates with predefined roles and environments to streamline + project setup +

+
+ + + + {(isAllowed) => ( + + )} +
- - )} - + + setEditTemplate(template)} + isOpen={popUp.addTemplate.isOpen} + onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can create project templates if you switch to Infisical's Enterprise plan." + /> +
+
+ )}
); }; diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx index 4ef07e81a..2d433bba8 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/UserDetailsByIDPage.tsx @@ -46,7 +46,7 @@ const Page = withPermission( }); const membershipId = search.membershipId as string; const { user } = useUser(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -131,7 +131,7 @@ const Page = withPermission( Users
{userId !== membership.user.id && ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index 680795e2e..bef98e875 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -35,7 +35,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => }); const { user } = useUser(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const userId = user?.id || ""; const orgId = currentOrg?.id || ""; @@ -214,7 +214,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>

-

)}
- {membership.isActive && + {!isSubOrganization && + membership.isActive && (membership.status === "invited" || membership.status === "verified") && membership.user.email && serverDetails?.emailConfigured && ( diff --git a/frontend/src/pages/organization/layout.tsx b/frontend/src/pages/organization/layout.tsx index 79bdbf216..0caf8286c 100644 --- a/frontend/src/pages/organization/layout.tsx +++ b/frontend/src/pages/organization/layout.tsx @@ -1,7 +1,14 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, retainSearchParams } from "@tanstack/react-router"; +import { z } from "zod"; import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({ - component: OrganizationLayout + component: OrganizationLayout, + validateSearch: z.object({ + subOrganization: z.string().optional() + }), + search: { + middlewares: [retainSearchParams(["subOrganization"])] + } }); diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx index 7261ba839..ce8ca2fbd 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/components/IdentityModal.tsx @@ -1,11 +1,16 @@ import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; +import { components, OptionProps } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { faGlobe } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { Link } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; import { + Badge, Button, FilterableSelect, FormControl, @@ -24,7 +29,7 @@ import { import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z.object({ - identity: z.object({ name: z.string(), id: z.string() }), + identity: z.object({ name: z.string(), id: z.string(), isManagedByRootOrg: z.boolean() }), role: z.object({ name: z.string(), slug: z.string() }) }); @@ -35,6 +40,29 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; }; +const Option = ({ + isSelected, + children, + ...props +}: OptionProps<{ name: string; id: string; isManagedByRootOrg: boolean }>) => { + return ( + +
+

{children}

+ {props.data.isManagedByRootOrg && ( + + + Managed by Root Org + + )} + {isSelected && ( + + )} +
+
+ ); +}; + const Content = ({ popUp, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const { projectId } = useProject(); @@ -131,9 +159,15 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => { value={value} onChange={onChange} placeholder="Select identity..." - options={filteredIdentityMembershipOrgs.map((membership) => membership.identity)} + options={filteredIdentityMembershipOrgs.map((membership) => ({ + ...membership.identity, + isManagedByRootOrg: membership.identity.orgId !== currentOrg.id + }))} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} + components={{ + Option + }} /> )} diff --git a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx index 4bdd3abc4..f865d2940 100644 --- a/frontend/src/pages/public/ErrorPage/ErrorPage.tsx +++ b/frontend/src/pages/public/ErrorPage/ErrorPage.tsx @@ -17,7 +17,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => { } return ( -
+

diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx index 54627888b..a34c08f9d 100644 --- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx +++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx @@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form"; import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useSearch } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -87,6 +88,10 @@ export const ShareSecretForm = ({ const [, isCopyingSecret, setCopyTextSecret] = useTimedReset({ initialState: "Copy to clipboard" }); + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); const publicSharedSecretCreator = useCreatePublicSharedSecret(); const privateSharedSecretCreator = useCreateSharedSecret(); @@ -148,11 +153,14 @@ export const ShareSecretForm = ({ type: "success" }); } else { - const link = `${window.location.origin}/shared/secret/${id}`; + const link = new URL(`${window.location.origin}/shared/secret/${id}`); + if (subOrganization) { + link.searchParams.set("subOrganization", subOrganization); + } - setSecretLink(link); + setSecretLink(link.toString()); - navigator.clipboard.writeText(link); + navigator.clipboard.writeText(link.toString()); setCopyTextSecret("secret"); createNotification({ diff --git a/frontend/src/pages/root.tsx b/frontend/src/pages/root.tsx index 42aea6d8f..ee47a5472 100644 --- a/frontend/src/pages/root.tsx +++ b/frontend/src/pages/root.tsx @@ -1,5 +1,6 @@ +import { useEffect } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; +import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router"; import { NotificationContainer } from "@app/components/notifications"; import { TooltipProvider } from "@app/components/v2"; @@ -13,6 +14,15 @@ type TRouterContext = { }; const RootPage = () => { + const subOrganization = useSearch({ + strict: false, + select: (el) => el?.subOrganization + }); + + useEffect(() => { + queryClient.invalidateQueries(); + }, [subOrganization]); + return ( diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 277baa912..853864c9b 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -56,7 +56,6 @@ import { Route as organizationAccessManagementPageRouteImport } from './pages/or import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route' import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page' import { Route as adminResourceOverviewPageRouteImport } from './pages/admin/ResourceOverviewPage/route' -import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route' import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route' import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route' import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route' @@ -752,14 +751,6 @@ const adminResourceOverviewPageRouteRoute = getParentRoute: () => adminLayoutRoute, } as any) -const organizationSecretSharingSettingsPageRouteRoute = - organizationSecretSharingSettingsPageRouteImport.update({ - id: '/settings', - path: '/settings', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute, - } as any) - const organizationRoleByIDPageRouteRoute = organizationRoleByIDPageRouteImport.update({ id: '/roles/$roleId', @@ -2625,13 +2616,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationRoleByIDPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } - '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' - path: '/settings' - fullPath: '/organization/secret-sharing/settings' - preLoaderRoute: typeof organizationSecretSharingSettingsPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport - } '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': { id: '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' path: '/resources/overview' @@ -4033,15 +4017,12 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationNetworkingRouteWithChildr interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren { organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute - organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren = { organizationSecretSharingPageRouteRoute: organizationSecretSharingPageRouteRoute, - organizationSecretSharingSettingsPageRouteRoute: - organizationSecretSharingSettingsPageRouteRoute, } const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren = @@ -5094,7 +5075,6 @@ export interface FileRoutesByFullPath { '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5328,7 +5308,6 @@ export interface FileRoutesByTo { '/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/admin/resources/overview': typeof adminResourceOverviewPageRouteRoute '/projects/cert-management/$projectId': typeof certManagerLayoutRouteWithChildren '/projects/kms/$projectId': typeof kmsLayoutRouteWithChildren @@ -5568,7 +5547,6 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview': typeof adminResourceOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsKmsProjectIdRouteWithChildren @@ -5818,7 +5796,6 @@ export interface FileRouteTypes { | '/organization/identities/$identityId' | '/organization/members/$membershipId' | '/organization/roles/$roleId' - | '/organization/secret-sharing/settings' | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' @@ -6051,7 +6028,6 @@ export interface FileRouteTypes { | '/organization/identities/$identityId' | '/organization/members/$membershipId' | '/organization/roles/$roleId' - | '/organization/secret-sharing/settings' | '/admin/resources/overview' | '/projects/cert-management/$projectId' | '/projects/kms/$projectId' @@ -6289,7 +6265,6 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId' - | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings' | '/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview' | '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId' | '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId' @@ -6814,8 +6789,7 @@ export const routeTree = rootRoute "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/organization", "children": [ - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/", - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings" + "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" ] }, "/_authenticate/_inject-org-details/_org-layout/organization/settings": { @@ -6865,10 +6839,6 @@ export const routeTree = rootRoute "filePath": "organization/RoleByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, - "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings": { - "filePath": "organization/SecretSharingSettingsPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing" - }, "/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview": { "filePath": "admin/ResourceOverviewPage/route.tsx", "parent": "/_authenticate/_inject-org-details/admin/_admin-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 860c677c1..8f48f6fe5 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -21,10 +21,7 @@ const organizationRoutes = route("/organization", [ route("/access-management", "organization/AccessManagementPage/route.tsx"), route("/audit-logs", "organization/AuditLogsPage/route.tsx"), route("/billing", "organization/BillingPage/route.tsx"), - route("/secret-sharing", [ - index("organization/SecretSharingPage/route.tsx"), - route("/settings", "organization/SecretSharingSettingsPage/route.tsx") - ]), + route("/secret-sharing", [index("organization/SecretSharingPage/route.tsx")]), route("/settings", [ index("organization/SettingsPage/route.tsx"), route("/oauth/callback", "organization/SettingsPage/OauthCallbackPage/route.tsx")