mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: first iteration changes for sub org routers
This commit is contained in:
3
backend/src/@types/fastify.d.ts
vendored
3
backend/src/@types/fastify.d.ts
vendored
@@ -126,6 +126,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||
import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service";
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
@@ -178,6 +179,7 @@ declare module "fastify" {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
};
|
||||
rateLimits: RateLimitConfiguration;
|
||||
// passport data
|
||||
@@ -327,6 +329,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
|
||||
|
||||
36
backend/src/db/migrations/20251018061215_sub-org.ts
Normal file
36
backend/src/db/migrations/20251018061215_sub-org.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||
if (!hasParentOrgId) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.uuid("parentOrgId");
|
||||
t.foreign("parentOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (!hasIdentityOrgCol) {
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||
if (hasParentOrgId) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("parentOrgId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (hasIdentityOrgCol) {
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.dropColumn("orgId");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<typeof IdentitiesSchema>;
|
||||
|
||||
@@ -312,6 +312,12 @@ export enum ActionProjectType {
|
||||
Any = "any"
|
||||
}
|
||||
|
||||
export enum OrganizationActionScope {
|
||||
ChildOrganization = "child-organization-only",
|
||||
ParentOrganization = "parent-organization-only",
|
||||
Any = "any"
|
||||
}
|
||||
|
||||
export enum TemporaryPermissionMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ 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()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
114
backend/src/ee/routes/v1/sub-org-router.ts
Normal file
114
backend/src/ee/routes/v1/sub-org-router.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SubOrganizations],
|
||||
description: "Create a child organization",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SUB_ORGANIZATIONS.CREATE.name)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { organization } = await server.services.subOrganization.createSubOrg({
|
||||
name: req.body.name,
|
||||
permissionActor: {
|
||||
id: req.permission.id,
|
||||
type: req.permission.type,
|
||||
authMethod: req.permission.authMethod,
|
||||
orgId: req.permission.orgId,
|
||||
parentOrgId: req.permission.parentOrgId
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_CHILD_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 child organizations",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().min(1).max(100).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: OrganizationsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { organizations } = await server.services.subOrganization.listSubOrgs({
|
||||
permissionActor: {
|
||||
id: req.permission.id,
|
||||
type: req.permission.type,
|
||||
authMethod: req.permission.authMethod,
|
||||
orgId: req.permission.orgId,
|
||||
parentOrgId: req.permission.orgId
|
||||
},
|
||||
data: {
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
isAccessible: req.query.isAccessible
|
||||
}
|
||||
});
|
||||
|
||||
return { organizations };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -173,6 +173,8 @@ export enum EventType {
|
||||
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
|
||||
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
|
||||
|
||||
CREATE_CHILD_ORGANIZATION = "create-child-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",
|
||||
@@ -607,6 +609,14 @@ interface GetSecretsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateChildOrganizationEvent {
|
||||
type: EventType.CREATE_CHILD_ORGANIZATION;
|
||||
metadata: {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
type TSecretMetadata = { key: string; value: string }[];
|
||||
|
||||
interface GetSecretEvent {
|
||||
@@ -3863,6 +3873,7 @@ interface PamResourceDeleteEvent {
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| CreateChildOrganizationEvent
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
rbac: false,
|
||||
githubOrgSync: false,
|
||||
customRateLimits: false,
|
||||
childOrganization: true,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
|
||||
@@ -33,6 +33,7 @@ export type TFeatureSet = {
|
||||
membersUsed: number;
|
||||
identityLimit: null;
|
||||
identitiesUsed: number;
|
||||
childOrganization: true;
|
||||
environmentLimit: null;
|
||||
environmentsUsed: 0;
|
||||
secretVersioning: true;
|
||||
|
||||
@@ -15,6 +15,11 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionChildOrgActions {
|
||||
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",
|
||||
ChildOrganization = "child-organization"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@@ -128,6 +134,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
||||
| [OrgPermissionChildOrgActions, OrgPermissionSubjects.ChildOrganization]
|
||||
| [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.ChildOrganization).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionChildOrgActions).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(OrgPermissionChildOrgActions.Create, OrgPermissionSubjects.ChildOrganization);
|
||||
can(OrgPermissionChildOrgActions.DirectAccess, OrgPermissionSubjects.ChildOrganization);
|
||||
|
||||
// role permission
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
|
||||
|
||||
@@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships {
|
||||
orgAuthEnforced?: boolean | null;
|
||||
orgGoogleSsoAuthEnforced?: boolean | null;
|
||||
shouldUseNewPrivilegeSystem?: boolean | null;
|
||||
parentOrgId?: boolean | 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("parentOrgId").withSchema(TableName.Organization).as("parentOrgId")
|
||||
);
|
||||
|
||||
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(),
|
||||
parentOrgId: z.boolean().optional().nullable(),
|
||||
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||
bypassOrgAuthEnabled: z.boolean()
|
||||
}).parse(el),
|
||||
|
||||
@@ -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;
|
||||
@@ -55,16 +40,11 @@ export type TGetOrgPermissionArg = {
|
||||
orgId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
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<OrgPermissionSet, MongoQuery>;
|
||||
memberships: Array<
|
||||
TMemberships & {
|
||||
|
||||
@@ -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 parentOrgId = permissionData?.[0]?.parentOrgId;
|
||||
const isChild = Boolean(parentOrgId);
|
||||
if (scope === OrganizationActionScope.ParentOrganization && isChild) {
|
||||
throw new BadRequestError({ message: `Child organization cannot do this operation` });
|
||||
} else if (scope === OrganizationActionScope.ChildOrganization && !isChild) {
|
||||
throw new BadRequestError({ 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),
|
||||
|
||||
109
backend/src/ee/services/sub-org/sub-org-service.ts
Normal file
109
backend/src/ee/services/sub-org/sub-org-service.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
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 { OrgPermissionChildOrgActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { TCreateSubOrgDTO, TListSubOrgDTO } from "./sub-org-types";
|
||||
|
||||
type TSubOrgServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "find" | "create" | "transaction" | "listSubOrganizations">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
membershipDAL: Pick<TMembershipDALFactory, "create">;
|
||||
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create">;
|
||||
};
|
||||
|
||||
export type TSubOrgServiceFactory = ReturnType<typeof subOrgServiceFactory>;
|
||||
|
||||
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(
|
||||
OrgPermissionChildOrgActions.Create,
|
||||
OrgPermissionSubjects.ChildOrganization
|
||||
);
|
||||
|
||||
const orgLicensePlan = await licenseService.getPlan(permissionActor.parentOrgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message: "Child organization creation failed. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const organization = await orgDAL.transaction(async (tx) => {
|
||||
const org = await orgDAL.create({ name, slug: name, 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.parentOrgId,
|
||||
actorOrgId: permissionActor.parentOrgId,
|
||||
actorAuthMethod: permissionActor.authMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
|
||||
const organizations = await orgDAL.listSubOrganizations({
|
||||
actorId: permissionActor.id,
|
||||
actorType: permissionActor.type,
|
||||
orgId: permissionActor.parentOrgId,
|
||||
isAccessible: data?.isAccessible,
|
||||
limit: data?.limit,
|
||||
offset: data?.offset
|
||||
});
|
||||
|
||||
return {
|
||||
organizations
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createSubOrg,
|
||||
listSubOrgs
|
||||
};
|
||||
};
|
||||
16
backend/src/ee/services/sub-org/sub-org-types.ts
Normal file
16
backend/src/ee/services/sub-org/sub-org-types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
}>;
|
||||
};
|
||||
@@ -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",
|
||||
@@ -716,6 +717,17 @@ export const ORGANIZATIONS = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const SUB_ORGANIZATIONS = {
|
||||
CREATE: {
|
||||
name: "The name of the child organization to create."
|
||||
},
|
||||
LIST: {
|
||||
limit: "The number of child organizations to return.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th child organization.",
|
||||
isAccessible: "Filter to only return child organizations that the actor has access to."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const PROJECTS = {
|
||||
CREATE: {
|
||||
organizationSlug: "The slug of the organization to create the project in.",
|
||||
|
||||
@@ -78,6 +78,7 @@ export type OrgServiceActor = {
|
||||
id: string;
|
||||
authMethod: ActorAuthMethod;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
};
|
||||
|
||||
export enum QueueWorkerProfile {
|
||||
|
||||
@@ -20,6 +20,7 @@ export type TAuthMode =
|
||||
tokenVersionId: string; // the session id of token used
|
||||
user: TUsers;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: AuthMethod;
|
||||
isMfaVerified?: boolean;
|
||||
token: AuthModeJwtTokenPayload;
|
||||
@@ -31,6 +32,7 @@ export type TAuthMode =
|
||||
userId: string;
|
||||
user: TUsers;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
@@ -39,6 +41,7 @@ export type TAuthMode =
|
||||
actor: ActorType.SERVICE;
|
||||
serviceTokenId: string;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: null;
|
||||
token: string;
|
||||
}
|
||||
@@ -48,6 +51,7 @@ export type TAuthMode =
|
||||
identityId: string;
|
||||
identityName: string;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: null;
|
||||
isInstanceAdmin?: boolean;
|
||||
token: TIdentityAccessTokenJwtPayload;
|
||||
@@ -57,6 +61,7 @@ export type TAuthMode =
|
||||
actor: ActorType.SCIM_CLIENT;
|
||||
scimTokenId: string;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: null;
|
||||
};
|
||||
|
||||
@@ -136,17 +141,24 @@ export const injectIdentity = fp(
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined;
|
||||
|
||||
switch (authMode) {
|
||||
case AuthMode.JWT: {
|
||||
const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token);
|
||||
const { user, tokenVersionId, orgId, 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,
|
||||
parentOrgId,
|
||||
authMethod: token.authMethod,
|
||||
isMfaVerified: token.isMfaVerified,
|
||||
token
|
||||
@@ -154,13 +166,18 @@ 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,
|
||||
parentOrgId: identity.parentOrgId,
|
||||
identityId: identity.identityId,
|
||||
identityName: identity.name,
|
||||
authMethod: null,
|
||||
@@ -190,8 +207,13 @@ 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,
|
||||
parentOrgId: serviceToken.orgId,
|
||||
authMode: AuthMode.SERVICE_TOKEN as const,
|
||||
serviceToken,
|
||||
serviceTokenId: serviceToken.id,
|
||||
@@ -202,22 +224,18 @@ 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: `Service token doesn't support sub organization selector` });
|
||||
|
||||
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null, parentOrgId: orgId };
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -14,7 +14,8 @@ 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
|
||||
parentOrgId: req.auth.parentOrgId
|
||||
};
|
||||
|
||||
logger.info(
|
||||
@@ -25,7 +26,8 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.IDENTITY,
|
||||
id: req.auth.identityId,
|
||||
orgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
authMethod: null,
|
||||
parentOrgId: req.auth.parentOrgId
|
||||
};
|
||||
|
||||
logger.info(
|
||||
@@ -36,6 +38,7 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.SERVICE,
|
||||
id: req.auth.serviceTokenId,
|
||||
orgId: req.auth.orgId,
|
||||
parentOrgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
@@ -47,6 +50,7 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.SCIM_CLIENT,
|
||||
id: req.auth.scimTokenId,
|
||||
orgId: req.auth.orgId,
|
||||
parentOrgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
|
||||
@@ -3,17 +3,19 @@ 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 { TUserDALFactory } from "../user/user-dal";
|
||||
import { TTokenDALFactory } from "./auth-token-dal";
|
||||
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
|
||||
type TAuthTokenServiceFactoryDep = {
|
||||
tokenDAL: TTokenDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
membershipUserDAL: Pick<TMembershipUserDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
@@ -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,53 @@ 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 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({
|
||||
parentOrgId: 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;
|
||||
parentOrgId = token.organizationId;
|
||||
} 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;
|
||||
parentOrgId = token.organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
|
||||
return { user, tokenVersionId: token.tokenVersionId, orgId, parentOrgId };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
|
||||
type TIdentityAccessTokenServiceFactoryDep = {
|
||||
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
||||
@@ -19,6 +20,7 @@ type TIdentityAccessTokenServiceFactoryDep = {
|
||||
"updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache"
|
||||
>;
|
||||
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
|
||||
@@ -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,36 @@ export const identityAccessTokenServiceFactory = ({
|
||||
trustedIps: trustedIps as TIp[]
|
||||
});
|
||||
}
|
||||
const identityOrgMembership = await membershipIdentityDAL.findOne({
|
||||
scope: AccessScope.Organization,
|
||||
actorIdentityId: identityAccessToken.identityId
|
||||
});
|
||||
let orgId = "";
|
||||
const parentOrgId = identityAccessToken.identityScopeOrgId;
|
||||
|
||||
if (!identityOrgMembership) {
|
||||
throw new BadRequestError({ message: "Identity does not belong to any organization" });
|
||||
if (subOrganizationSelector) {
|
||||
const subOrganization = await orgDAL.findOne({ parentOrgId, slug: subOrganizationSelector });
|
||||
if (!subOrganizationSelector)
|
||||
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;
|
||||
} else {
|
||||
const identityOrgMembership = await membershipIdentityDAL.findOne({
|
||||
scope: AccessScope.Organization,
|
||||
actorIdentityId: identityAccessToken.identityId,
|
||||
scopeOrgId: parentOrgId
|
||||
});
|
||||
|
||||
if (!identityOrgMembership) {
|
||||
throw new BadRequestError({ message: "Identity does not belong to any organization" });
|
||||
}
|
||||
|
||||
orgId = parentOrgId;
|
||||
}
|
||||
|
||||
let { accessTokenNumUses } = identityAccessToken;
|
||||
@@ -219,7 +249,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
|
||||
|
||||
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
|
||||
return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId };
|
||||
return { ...identityAccessToken, orgId, parentOrgId };
|
||||
};
|
||||
|
||||
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
|
||||
|
||||
import { OrgAuthMethod } from "./org-types";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
|
||||
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
|
||||
|
||||
@@ -64,6 +65,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => {
|
||||
return db
|
||||
.replicaNode()(TableName.Organization)
|
||||
.whereNull(`${TableName.Organization}.parentOrgId`)
|
||||
.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,47 @@ 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}.parentOrgId`, dto.orgId)
|
||||
.select(selectAllTableCols(TableName.Organization));
|
||||
|
||||
if (dto.isAccessible) {
|
||||
void query.leftJoin(`${TableName.Membership}`, (qb) => {
|
||||
void qb.on(`${TableName.Membership}.scope`, AccessScope.Organization);
|
||||
if (dto.actorType === ActorType.IDENTITY) {
|
||||
void qb.andOn(`${TableName.Membership}.actorIdentityId`, dto.actorId);
|
||||
} else {
|
||||
void qb.andOn(`${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}.parentOrgId`)
|
||||
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
`${TableName.SamlConfig}.isActive`,
|
||||
@@ -195,6 +233,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const org = (await db
|
||||
.replicaNode()(TableName.Organization)
|
||||
.whereNull(`${TableName.Organization}.parentOrgId`)
|
||||
.where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug })
|
||||
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
@@ -240,6 +279,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}.parentOrgId`)
|
||||
.leftJoin(TableName.SamlConfig, (qb) => {
|
||||
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
|
||||
`${TableName.SamlConfig}.isActive`,
|
||||
@@ -337,6 +377,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(sub-org): updated this logic later
|
||||
const countAllOrgMembers = async (orgId: string) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
@@ -610,6 +651,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}.parentOrgId`)
|
||||
.leftJoin(TableName.UserAliases, function joinUserAlias() {
|
||||
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`)
|
||||
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`)
|
||||
@@ -648,6 +690,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.replicaNode()(TableName.Membership)
|
||||
.where({ actorIdentityId: identityId })
|
||||
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
|
||||
.whereNull(`${TableName.Organization}.parentOrgId`)
|
||||
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
|
||||
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
|
||||
.join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`)
|
||||
@@ -667,6 +710,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
findOrgByProjectId,
|
||||
findAllOrgMembers,
|
||||
countAllOrgMembers,
|
||||
listSubOrganizations,
|
||||
findOrgById,
|
||||
findOrgBySlug,
|
||||
findAllOrgsByUserId,
|
||||
|
||||
Reference in New Issue
Block a user