feat: first iteration changes for sub org routers

This commit is contained in:
=
2025-10-18 15:30:21 +05:30
parent 00c19455af
commit ef6282968b
23 changed files with 529 additions and 77 deletions

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 { 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

View 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");
});
}
}

View File

@@ -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>;

View File

@@ -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"
}

View File

@@ -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>;

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 };
}
});
};

View File

@@ -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

View File

@@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
rbac: false,
githubOrgSync: false,
customRateLimits: false,
childOrganization: true,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,

View File

@@ -33,6 +33,7 @@ export type TFeatureSet = {
membersUsed: number;
identityLimit: null;
identitiesUsed: number;
childOrganization: true;
environmentLimit: null;
environmentsUsed: 0;
secretVersioning: true;

View File

@@ -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);

View File

@@ -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),

View File

@@ -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 & {

View File

@@ -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),

View 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
};
};

View 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;
}>;
};

View File

@@ -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.",

View File

@@ -78,6 +78,7 @@ export type OrgServiceActor = {
id: string;
authMethod: ActorAuthMethod;
orgId: string;
parentOrgId: string;
};
export enum QueueWorkerProfile {

View File

@@ -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:

View File

@@ -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
};

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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,