mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 02:51:51 +00:00
feat: first iteration changes for sub org routers
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
};
|
||||
Reference in New Issue
Block a user