feat: first iteration changes for sub org routers

This commit is contained in:
=
2025-10-20 18:57:03 +05:30
parent 00c19455af
commit ef6282968b
23 changed files with 529 additions and 77 deletions
+3
View File
@@ -126,6 +126,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-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" { declare module "@fastify/request-context" {
interface RequestContextData { interface RequestContextData {
@@ -178,6 +179,7 @@ declare module "fastify" {
type: ActorType; type: ActorType;
id: string; id: string;
orgId: string; orgId: string;
parentOrgId: string;
}; };
rateLimits: RateLimitConfiguration; rateLimits: RateLimitConfiguration;
// passport data // passport data
@@ -327,6 +329,7 @@ declare module "fastify" {
additionalPrivilege: TAdditionalPrivilegeServiceFactory; additionalPrivilege: TAdditionalPrivilegeServiceFactory;
role: TRoleServiceFactory; role: TRoleServiceFactory;
convertor: TConvertorServiceFactory; convertor: TConvertorServiceFactory;
subOrganization: TSubOrgServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
@@ -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");
});
}
}
+2 -1
View File
@@ -13,7 +13,8 @@ export const IdentitiesSchema = z.object({
authMethod: z.string().nullable().optional(), authMethod: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: 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>; export type TIdentities = z.infer<typeof IdentitiesSchema>;
+6
View File
@@ -312,6 +312,12 @@ export enum ActionProjectType {
Any = "any" Any = "any"
} }
export enum OrganizationActionScope {
ChildOrganization = "child-organization-only",
ParentOrganization = "parent-organization-only",
Any = "any"
}
export enum TemporaryPermissionMode { export enum TemporaryPermissionMode {
Relative = "relative" Relative = "relative"
} }
+2 -1
View File
@@ -38,7 +38,8 @@ export const OrganizationsSchema = z.object({
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
maxSharedSecretViewLimit: z.number().nullable().optional(), maxSharedSecretViewLimit: z.number().nullable().optional(),
googleSsoAuthEnforced: z.boolean().default(false), 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>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+114
View 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", UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-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", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-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 }[]; type TSecretMetadata = { key: string; value: string }[];
interface GetSecretEvent { interface GetSecretEvent {
@@ -3863,6 +3873,7 @@ interface PamResourceDeleteEvent {
} }
export type Event = export type Event =
| CreateChildOrganizationEvent
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
| CreateSecretEvent | CreateSecretEvent
@@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
rbac: false, rbac: false,
githubOrgSync: false, githubOrgSync: false,
customRateLimits: false, customRateLimits: false,
childOrganization: true,
customAlerts: false, customAlerts: false,
secretAccessInsights: false, secretAccessInsights: false,
auditLogs: false, auditLogs: false,
@@ -33,6 +33,7 @@ export type TFeatureSet = {
membersUsed: number; membersUsed: number;
identityLimit: null; identityLimit: null;
identitiesUsed: number; identitiesUsed: number;
childOrganization: true;
environmentLimit: null; environmentLimit: null;
environmentsUsed: 0; environmentsUsed: 0;
secretVersioning: true; secretVersioning: true;
@@ -15,6 +15,11 @@ export enum OrgPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum OrgPermissionChildOrgActions {
Create = "create",
DirectAccess = "direct-access"
}
export enum OrgPermissionAppConnectionActions { export enum OrgPermissionAppConnectionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -117,7 +122,8 @@ export enum OrgPermissionSubjects {
Kmip = "kmip", Kmip = "kmip",
Gateway = "gateway", Gateway = "gateway",
Relay = "relay", Relay = "relay",
SecretShare = "secret-share" SecretShare = "secret-share",
ChildOrganization = "child-organization"
} }
export type AppConnectionSubjectFields = { export type AppConnectionSubjectFields = {
@@ -128,6 +134,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Project] | [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
| [OrgPermissionActions, OrgPermissionSubjects.Role] | [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionChildOrgActions, OrgPermissionSubjects.ChildOrganization]
| [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [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."), 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.") 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({ z.object({
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."), 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.") action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
@@ -308,6 +321,10 @@ const buildAdminPermission = () => {
// ws permissions // ws permissions
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); can(OrgPermissionActions.Create, OrgPermissionSubjects.Project);
can(OrgPermissionChildOrgActions.Create, OrgPermissionSubjects.ChildOrganization);
can(OrgPermissionChildOrgActions.DirectAccess, OrgPermissionSubjects.ChildOrganization);
// role permission // role permission
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
@@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships {
orgAuthEnforced?: boolean | null; orgAuthEnforced?: boolean | null;
orgGoogleSsoAuthEnforced?: boolean | null; orgGoogleSsoAuthEnforced?: boolean | null;
shouldUseNewPrivilegeSystem?: boolean | null; shouldUseNewPrivilegeSystem?: boolean | null;
parentOrgId?: boolean | null;
bypassOrgAuthEnabled?: boolean | null; bypassOrgAuthEnabled?: boolean | null;
roles: { roles: {
id: string; id: string;
@@ -273,7 +274,8 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), 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({ const data = sqlNestRelationships({
@@ -283,6 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
MembershipsSchema.extend({ MembershipsSchema.extend({
orgAuthEnforced: z.boolean().optional().nullable(), orgAuthEnforced: z.boolean().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(),
parentOrgId: z.boolean().optional().nullable(),
orgGoogleSsoAuthEnforced: z.boolean(), orgGoogleSsoAuthEnforced: z.boolean(),
bypassOrgAuthEnabled: z.boolean() bypassOrgAuthEnabled: z.boolean()
}).parse(el), }).parse(el),
@@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js"; import { MongoQuery } from "@ucast/mongo2js";
import { Knex } from "knex"; 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 { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionSet } from "./org-permission"; import { OrgPermissionSet } from "./org-permission";
@@ -18,21 +18,6 @@ export type TBuildOrgPermissionDTO = {
role: string; 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 = { export type TGetServiceTokenProjectPermissionArg = {
serviceTokenId: string; serviceTokenId: string;
projectId: string; projectId: string;
@@ -55,16 +40,11 @@ export type TGetOrgPermissionArg = {
orgId: string; orgId: string;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId?: string; actorOrgId?: string;
scope: OrganizationActionScope;
}; };
export type TPermissionServiceFactory = { export type TPermissionServiceFactory = {
getOrgPermission: ( getOrgPermission: (arg: TGetOrgPermissionArg) => Promise<{
type: ActorType,
id: string,
orgId: string,
authMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => Promise<{
permission: MongoAbility<OrgPermissionSet, MongoQuery>; permission: MongoAbility<OrgPermissionSet, MongoQuery>;
memberships: Array< memberships: Array<
TMemberships & { TMemberships & {
@@ -7,6 +7,7 @@ import { Knex } from "knex";
import { import {
AccessScope, AccessScope,
ActionProjectType, ActionProjectType,
OrganizationActionScope,
OrgMembershipRole, OrgMembershipRole,
ProjectMembershipRole, ProjectMembershipRole,
ServiceTokenScopes ServiceTokenScopes
@@ -179,14 +180,15 @@ export const permissionServiceFactory = ({
// return minTtl; // return minTtl;
// }; // };
const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ({
type, actor,
id, actorId,
orgId, orgId,
authMethod, actorOrgId,
actorOrgId scope,
) => { actorAuthMethod
if (type !== ActorType.USER && type !== ActorType.IDENTITY) { }) => {
if (actor !== ActorType.USER && actor !== ActorType.IDENTITY) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid actor provided", message: "Invalid actor provided",
name: "Get org permission" name: "Get org permission"
@@ -202,11 +204,19 @@ export const permissionServiceFactory = ({
scope: AccessScope.Organization, scope: AccessScope.Organization,
orgId orgId
}, },
actorId: id, actorId,
actorType: type actorType: actor
}); });
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); 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 permissionFromRoles = permissionData.flatMap((membership) => {
const activeRoles = membership?.roles const activeRoles = membership?.roles
.filter( .filter(
@@ -227,7 +237,7 @@ export const permissionServiceFactory = ({
permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role)));
validateOrgSSO( validateOrgSSO(
authMethod, actorAuthMethod,
permissionData?.[0].orgAuthEnforced, permissionData?.[0].orgAuthEnforced,
Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced),
Boolean(permissionData?.[0].bypassOrgAuthEnabled), Boolean(permissionData?.[0].bypassOrgAuthEnabled),
@@ -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
};
};
@@ -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;
}>;
};
+12
View File
@@ -33,6 +33,7 @@ export enum ApiDocsTags {
LdapAuth = "LDAP Auth", LdapAuth = "LDAP Auth",
Groups = "Groups", Groups = "Groups",
Organizations = "Organizations", Organizations = "Organizations",
SubOrganizations = "Sub Organizations",
Projects = "Projects", Projects = "Projects",
ProjectUsers = "Project Users", ProjectUsers = "Project Users",
ProjectGroups = "Project Groups", ProjectGroups = "Project Groups",
@@ -716,6 +717,17 @@ export const ORGANIZATIONS = {
} }
} as const; } 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 = { export const PROJECTS = {
CREATE: { CREATE: {
organizationSlug: "The slug of the organization to create the project in.", organizationSlug: "The slug of the organization to create the project in.",
+1
View File
@@ -78,6 +78,7 @@ export type OrgServiceActor = {
id: string; id: string;
authMethod: ActorAuthMethod; authMethod: ActorAuthMethod;
orgId: string; orgId: string;
parentOrgId: string;
}; };
export enum QueueWorkerProfile { export enum QueueWorkerProfile {
@@ -20,6 +20,7 @@ export type TAuthMode =
tokenVersionId: string; // the session id of token used tokenVersionId: string; // the session id of token used
user: TUsers; user: TUsers;
orgId: string; orgId: string;
parentOrgId: string;
authMethod: AuthMethod; authMethod: AuthMethod;
isMfaVerified?: boolean; isMfaVerified?: boolean;
token: AuthModeJwtTokenPayload; token: AuthModeJwtTokenPayload;
@@ -31,6 +32,7 @@ export type TAuthMode =
userId: string; userId: string;
user: TUsers; user: TUsers;
orgId: string; orgId: string;
parentOrgId: string;
token: string; token: string;
} }
| { | {
@@ -39,6 +41,7 @@ export type TAuthMode =
actor: ActorType.SERVICE; actor: ActorType.SERVICE;
serviceTokenId: string; serviceTokenId: string;
orgId: string; orgId: string;
parentOrgId: string;
authMethod: null; authMethod: null;
token: string; token: string;
} }
@@ -48,6 +51,7 @@ export type TAuthMode =
identityId: string; identityId: string;
identityName: string; identityName: string;
orgId: string; orgId: string;
parentOrgId: string;
authMethod: null; authMethod: null;
isInstanceAdmin?: boolean; isInstanceAdmin?: boolean;
token: TIdentityAccessTokenJwtPayload; token: TIdentityAccessTokenJwtPayload;
@@ -57,6 +61,7 @@ export type TAuthMode =
actor: ActorType.SCIM_CLIENT; actor: ActorType.SCIM_CLIENT;
scimTokenId: string; scimTokenId: string;
orgId: string; orgId: string;
parentOrgId: string;
authMethod: null; authMethod: null;
}; };
@@ -136,17 +141,24 @@ export const injectIdentity = fp(
if (!authMode) return; if (!authMode) return;
const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined;
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { 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); requestContext.set("orgId", orgId);
req.auth = { req.auth = {
authMode: AuthMode.JWT, authMode: AuthMode.JWT,
user, user,
userId: user.id, userId: user.id,
tokenVersionId, tokenVersionId,
actor, actor,
orgId: orgId as string, orgId,
parentOrgId,
authMethod: token.authMethod, authMethod: token.authMethod,
isMfaVerified: token.isMfaVerified, isMfaVerified: token.isMfaVerified,
token token
@@ -154,13 +166,18 @@ export const injectIdentity = fp(
break; break;
} }
case AuthMode.IDENTITY_ACCESS_TOKEN: { 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(); const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId); requestContext.set("orgId", identity.orgId);
req.auth = { req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor, actor,
orgId: identity.orgId, orgId: identity.orgId,
parentOrgId: identity.parentOrgId,
identityId: identity.identityId, identityId: identity.identityId,
identityName: identity.name, identityName: identity.name,
authMethod: null, authMethod: null,
@@ -190,8 +207,13 @@ export const injectIdentity = fp(
case AuthMode.SERVICE_TOKEN: { case AuthMode.SERVICE_TOKEN: {
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
requestContext.set("orgId", serviceToken.orgId); requestContext.set("orgId", serviceToken.orgId);
if (subOrganizationSelector)
throw new BadRequestError({ message: `Service token doesn't support sub organization selector` });
req.auth = { req.auth = {
orgId: serviceToken.orgId, orgId: serviceToken.orgId,
parentOrgId: serviceToken.orgId,
authMode: AuthMode.SERVICE_TOKEN as const, authMode: AuthMode.SERVICE_TOKEN as const,
serviceToken, serviceToken,
serviceTokenId: serviceToken.id, serviceTokenId: serviceToken.id,
@@ -202,22 +224,18 @@ export const injectIdentity = fp(
break; break;
} }
case AuthMode.API_KEY: { case AuthMode.API_KEY: {
const user = await server.services.apiKey.fnValidateApiKey(token as string); throw new BadRequestError({
req.auth = { message: "API key authentication is not supported anymore. Please switch to identity authentication."
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;
} }
case AuthMode.SCIM_TOKEN: { case AuthMode.SCIM_TOKEN: {
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
requestContext.set("orgId", orgId); 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; break;
} }
default: default:
@@ -14,7 +14,8 @@ export const injectPermission = fp(async (server) => {
type: ActorType.USER, type: ActorType.USER,
id: req.auth.userId, id: req.auth.userId,
orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" 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( logger.info(
@@ -25,7 +26,8 @@ export const injectPermission = fp(async (server) => {
type: ActorType.IDENTITY, type: ActorType.IDENTITY,
id: req.auth.identityId, id: req.auth.identityId,
orgId: req.auth.orgId, orgId: req.auth.orgId,
authMethod: null authMethod: null,
parentOrgId: req.auth.parentOrgId
}; };
logger.info( logger.info(
@@ -36,6 +38,7 @@ export const injectPermission = fp(async (server) => {
type: ActorType.SERVICE, type: ActorType.SERVICE,
id: req.auth.serviceTokenId, id: req.auth.serviceTokenId,
orgId: req.auth.orgId, orgId: req.auth.orgId,
parentOrgId: req.auth.orgId,
authMethod: null authMethod: null
}; };
@@ -47,6 +50,7 @@ export const injectPermission = fp(async (server) => {
type: ActorType.SCIM_CLIENT, type: ActorType.SCIM_CLIENT,
id: req.auth.scimTokenId, id: req.auth.scimTokenId,
orgId: req.auth.orgId, orgId: req.auth.orgId,
parentOrgId: req.auth.orgId,
authMethod: null authMethod: null
}; };
@@ -3,17 +3,19 @@ import { Knex } from "knex";
import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; 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 { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal"; import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
import { TOrgDALFactory } from "../org/org-dal";
type TAuthTokenServiceFactoryDep = { type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory; tokenDAL: TTokenDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "transaction">; userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "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 createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type); const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig(); const appCfg = getConfig();
@@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
}; };
// to parse jwt identity in inject identity plugin // 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({ const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId, id: token.tokenVersionId,
userId: token.userId userId: token.userId
@@ -207,22 +209,53 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
const user = await userDAL.findById(session.userId); const user = await userDAL.findById(session.userId);
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
let orgId = "";
let parentOrgId = "";
if (token.organizationId) { if (token.organizationId) {
const orgMembership = await membershipUserDAL.findOne({ if (subOrganizationSelector) {
actorUserId: user.id, const subOrganization = await orgDAL.findOne({
scopeOrgId: token.organizationId, parentOrgId: token.organizationId,
scope: AccessScope.Organization slug: subOrganizationSelector
}); });
if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
if (!orgMembership) { const orgMembership = await membershipUserDAL.findOne({
throw new ForbiddenRequestError({ message: "User not member of organization" }); actorUserId: user.id,
} scopeOrgId: subOrganization.id,
if (!orgMembership.isActive) { scope: AccessScope.Organization
throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); });
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 { return {
@@ -19,6 +19,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.select(selectAllTableCols(TableName.IdentityAccessToken)) .select(selectAllTableCols(TableName.IdentityAccessToken))
.select(db.ref("name").withSchema(TableName.Identity)) .select(db.ref("name").withSchema(TableName.Identity))
.select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId"))
.first(); .first();
return doc; return doc;
@@ -10,6 +10,7 @@ import { TIdentityDALFactory } from "../identity/identity-dal";
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
import { TOrgDALFactory } from "../org/org-dal";
type TIdentityAccessTokenServiceFactoryDep = { type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
@@ -19,6 +20,7 @@ type TIdentityAccessTokenServiceFactoryDep = {
"updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache" "updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache"
>; >;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne">; membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
}; };
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>; export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
@@ -27,7 +29,8 @@ export const identityAccessTokenServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
accessTokenQueue, accessTokenQueue,
identityDAL, identityDAL,
membershipIdentityDAL membershipIdentityDAL,
orgDAL
}: TIdentityAccessTokenServiceFactoryDep) => { }: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
const { const {
@@ -181,7 +184,11 @@ export const identityAccessTokenServiceFactory = ({
return { revokedToken }; return { revokedToken };
}; };
const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { const fnValidateIdentityAccessToken = async (
token: TIdentityAccessTokenJwtPayload,
subOrganizationSelector?: string,
ipAddress?: string
) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({ const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId,
isAccessTokenRevoked: false isAccessTokenRevoked: false
@@ -202,13 +209,36 @@ export const identityAccessTokenServiceFactory = ({
trustedIps: trustedIps as TIp[] trustedIps: trustedIps as TIp[]
}); });
} }
const identityOrgMembership = await membershipIdentityDAL.findOne({ let orgId = "";
scope: AccessScope.Organization, const parentOrgId = identityAccessToken.identityScopeOrgId;
actorIdentityId: identityAccessToken.identityId
});
if (!identityOrgMembership) { if (subOrganizationSelector) {
throw new BadRequestError({ message: "Identity does not belong to any organization" }); 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; let { accessTokenNumUses } = identityAccessToken;
@@ -219,7 +249,7 @@ export const identityAccessTokenServiceFactory = ({
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId }; return { ...identityAccessToken, orgId, parentOrgId };
}; };
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
+44
View File
@@ -27,6 +27,7 @@ import {
import { generateKnexQueryFromScim } from "@app/lib/knex/scim"; import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
import { OrgAuthMethod } from "./org-types"; import { OrgAuthMethod } from "./org-types";
import { ActorType } from "../auth/auth-type";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>; export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
@@ -64,6 +65,7 @@ export const orgDALFactory = (db: TDbClient) => {
const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => { const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => {
return db return db
.replicaNode()(TableName.Organization) .replicaNode()(TableName.Organization)
.whereNull(`${TableName.Organization}.parentOrgId`)
.whereIn(`${TableName.Organization}.id`, orgIdSubquery) .whereIn(`${TableName.Organization}.id`, orgIdSubquery)
.leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) .leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .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) => { const findOrgById = async (orgId: string) => {
try { try {
const org = (await db const org = (await db
.replicaNode()(TableName.Organization) .replicaNode()(TableName.Organization)
.where({ [`${TableName.Organization}.id` as "id"]: orgId }) .where({ [`${TableName.Organization}.id` as "id"]: orgId })
.whereNull(`${TableName.Organization}.parentOrgId`)
.leftJoin(TableName.SamlConfig, (qb) => { .leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.SamlConfig}.isActive`, `${TableName.SamlConfig}.isActive`,
@@ -195,6 +233,7 @@ export const orgDALFactory = (db: TDbClient) => {
try { try {
const org = (await db const org = (await db
.replicaNode()(TableName.Organization) .replicaNode()(TableName.Organization)
.whereNull(`${TableName.Organization}.parentOrgId`)
.where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug }) .where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug })
.leftJoin(TableName.SamlConfig, (qb) => { .leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
@@ -240,6 +279,7 @@ export const orgDALFactory = (db: TDbClient) => {
.whereNotNull(`${TableName.Membership}.actorUserId`) .whereNotNull(`${TableName.Membership}.actorUserId`)
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
.join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`)
.whereNull(`${TableName.Organization}.parentOrgId`)
.leftJoin(TableName.SamlConfig, (qb) => { .leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn( qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.SamlConfig}.isActive`, `${TableName.SamlConfig}.isActive`,
@@ -337,6 +377,7 @@ export const orgDALFactory = (db: TDbClient) => {
} }
}; };
// TODO(sub-org): updated this logic later
const countAllOrgMembers = async (orgId: string) => { const countAllOrgMembers = async (orgId: string) => {
try { try {
interface CountResult { interface CountResult {
@@ -610,6 +651,7 @@ export const orgDALFactory = (db: TDbClient) => {
}) })
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`) .join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`) .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
.whereNull(`${TableName.Organization}.parentOrgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() { .leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`) this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`) .andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`)
@@ -648,6 +690,7 @@ export const orgDALFactory = (db: TDbClient) => {
.replicaNode()(TableName.Membership) .replicaNode()(TableName.Membership)
.where({ actorIdentityId: identityId }) .where({ actorIdentityId: identityId })
.where(`${TableName.Membership}.scope`, AccessScope.Organization) .where(`${TableName.Membership}.scope`, AccessScope.Organization)
.whereNull(`${TableName.Organization}.parentOrgId`)
.whereNotNull(`${TableName.Membership}.actorIdentityId`) .whereNotNull(`${TableName.Membership}.actorIdentityId`)
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`) .join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
.join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`) .join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`)
@@ -667,6 +710,7 @@ export const orgDALFactory = (db: TDbClient) => {
findOrgByProjectId, findOrgByProjectId,
findAllOrgMembers, findAllOrgMembers,
countAllOrgMembers, countAllOrgMembers,
listSubOrganizations,
findOrgById, findOrgById,
findOrgBySlug, findOrgBySlug,
findAllOrgsByUserId, findAllOrgsByUserId,