chore: merge main

This commit is contained in:
Scott Wilson
2025-12-09 10:49:52 -08:00
123 changed files with 1985 additions and 4022 deletions
+544 -3372
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -91,7 +91,7 @@
"@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-attributes": "^7.24.7",
"@babel/preset-env": "^7.18.10", "@babel/preset-env": "^7.18.10",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@react-email/preview-server": "^4.3.0", "@react-email/preview-server": "^5.0.6",
"@smithy/types": "^4.3.1", "@smithy/types": "^4.3.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
@@ -129,7 +129,7 @@
"nodemon": "^3.0.2", "nodemon": "^3.0.2",
"pino-pretty": "^10.2.3", "pino-pretty": "^10.2.3",
"prompt-sync": "^4.2.0", "prompt-sync": "^4.2.0",
"react-email": "^4.3.0", "react-email": "^5.0.6",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsc-alias": "^1.8.8", "tsc-alias": "^1.8.8",
@@ -184,7 +184,7 @@
"@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/semantic-conventions": "^1.27.0",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.12.1", "@peculiar/x509": "^1.12.1",
"@react-email/components": "0.0.36", "@react-email/components": "^1.0.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.2", "@slack/oauth": "^3.0.2",
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSubOrganizationIdColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "subOrganizationId");
if (!hasSubOrganizationIdColumn) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.uuid("subOrganizationId").nullable();
t.foreign("subOrganizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSubOrganizationIdColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "subOrganizationId");
if (hasSubOrganizationIdColumn) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.dropColumn("subOrganizationId");
});
}
}
@@ -22,7 +22,8 @@ export const IdentityAccessTokensSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
authMethod: z.string(), authMethod: z.string(),
accessTokenPeriod: z.coerce.number().default(0) accessTokenPeriod: z.coerce.number().default(0),
subOrganizationId: z.string().uuid().nullable().optional()
}); });
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>; export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
+4 -2
View File
@@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const plan = await server.services.license.getOrgPlan({ const plan = await server.services.license.getOrgPlan({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.rootOrgId, actorOrgId: req.permission.orgId,
rootOrgId: req.permission.rootOrgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId, orgId: req.params.organizationId,
refreshCache: req.query.refreshCache refreshCache: req.query.refreshCache
@@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId orgId: req.params.organizationId,
rootOrgId: req.permission.rootOrgId
}); });
return data; return data;
} }
@@ -368,6 +368,7 @@ export enum EventType {
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
USER_LOGIN = "user-login", USER_LOGIN = "user-login",
SELECT_ORGANIZATION = "select-organization", SELECT_ORGANIZATION = "select-organization",
SELECT_SUB_ORGANIZATION = "select-sub-organization",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -2704,6 +2705,15 @@ interface SelectOrganizationEvent {
}; };
} }
interface SelectSubOrganizationEvent {
type: EventType.SELECT_SUB_ORGANIZATION;
metadata: {
organizationId: string;
organizationName: string;
rootOrganizationId: string;
};
}
interface CreateCertificateTemplateEstConfig { interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: { metadata: {
@@ -4744,6 +4754,7 @@ export type Event =
| AutomatedRenewCertificateFailed | AutomatedRenewCertificateFailed
| UserLoginEvent | UserLoginEvent
| SelectOrganizationEvent | SelectOrganizationEvent
| SelectSubOrganizationEvent
| ApprovalPolicyCreateEvent | ApprovalPolicyCreateEvent
| ApprovalPolicyUpdateEvent | ApprovalPolicyUpdateEvent
| ApprovalPolicyDeleteEvent | ApprovalPolicyDeleteEvent
@@ -350,6 +350,7 @@ export const licenseServiceFactory = ({
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
rootOrgId,
actorAuthMethod, actorAuthMethod,
projectId, projectId,
refreshCache refreshCache
@@ -360,12 +361,12 @@ export const licenseServiceFactory = ({
orgId, orgId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization scope: OrganizationActionScope.Any
}); });
if (refreshCache) { if (refreshCache) {
await refreshPlan(orgId); await refreshPlan(rootOrgId);
} }
const plan = await getPlan(orgId, projectId); const plan = await getPlan(rootOrgId, projectId);
return plan; return plan;
}; };
@@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = {
export type TOrgPlanDTO = { export type TOrgPlanDTO = {
projectId?: string; projectId?: string;
refreshCache?: boolean; refreshCache?: boolean;
rootOrgId: string;
} & TOrgPermission; } & TOrgPermission;
export type TStartOrgTrialDTO = { export type TStartOrgTrialDTO = {
+26 -12
View File
@@ -170,10 +170,13 @@ export const IDENTITIES = {
} }
} as const; } as const;
const IDENTITY_AUTH_SUB_ORGANIZATION_NAME = "sub-organization name to scope the token to";
export const UNIVERSAL_AUTH = { export const UNIVERSAL_AUTH = {
LOGIN: { LOGIN: {
clientId: "Your Machine Identity Client ID.", clientId: "Your Machine Identity Client ID.",
clientSecret: "Your Machine Identity Client Secret." clientSecret: "Your Machine Identity Client Secret.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -247,7 +250,8 @@ export const LDAP_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login.", identityId: "The ID of the machine identity to login.",
username: "The username of the LDAP user to login.", username: "The username of the LDAP user to login.",
password: "The password of the LDAP user to login." password: "The password of the LDAP user to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.", templateId: "The ID of the identity auth template to attach the configuration onto.",
@@ -312,7 +316,8 @@ export const ALICLOUD_AUTH = {
Timestamp: "The timestamp of the request in UTC, formatted as 'YYYY-MM-DDTHH:mm:ssZ'.", Timestamp: "The timestamp of the request in UTC, formatted as 'YYYY-MM-DDTHH:mm:ssZ'.",
SignatureVersion: "The signature version. For STS GetCallerIdentity, this should be '1.0'.", SignatureVersion: "The signature version. For STS GetCallerIdentity, this should be '1.0'.",
SignatureNonce: "A unique random string to prevent replay attacks.", SignatureNonce: "A unique random string to prevent replay attacks.",
Signature: "The signature string calculated based on the request parameters and AccessKey Secret." Signature: "The signature string calculated based on the request parameters and AccessKey Secret.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -340,7 +345,8 @@ export const ALICLOUD_AUTH = {
export const TLS_CERT_AUTH = { export const TLS_CERT_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login." identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -378,7 +384,8 @@ export const AWS_AUTH = {
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.", "The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.",
iamRequestBody: iamRequestBody:
"The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.", "The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.",
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request." iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -416,7 +423,8 @@ export const OCI_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login.", identityId: "The ID of the machine identity to login.",
userOcid: "The OCID of the user attempting login.", userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request." headers: "The headers of the signed request.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -448,7 +456,8 @@ export const OCI_AUTH = {
export const AZURE_AUTH = { export const AZURE_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login." identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -482,7 +491,8 @@ export const AZURE_AUTH = {
export const GCP_AUTH = { export const GCP_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login." identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -520,7 +530,8 @@ export const GCP_AUTH = {
export const KUBERNETES_AUTH = { export const KUBERNETES_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login." identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -600,7 +611,8 @@ export const TOKEN_AUTH = {
}, },
CREATE_TOKEN: { CREATE_TOKEN: {
identityId: "The ID of the machine identity to create the token for.", identityId: "The ID of the machine identity to create the token for.",
name: "The name of the token to create." name: "The name of the token to create.",
subOrganizationName: "The sub organization name to scope the token to."
}, },
UPDATE_TOKEN: { UPDATE_TOKEN: {
tokenId: "The ID of the token to update metadata for.", tokenId: "The ID of the token to update metadata for.",
@@ -613,7 +625,8 @@ export const TOKEN_AUTH = {
export const OIDC_AUTH = { export const OIDC_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login." identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -653,7 +666,8 @@ export const OIDC_AUTH = {
export const JWT_AUTH = { export const JWT_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the machine identity to login." identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -8,7 +8,6 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { slugSchema } from "@app/server/lib/schemas";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -152,15 +151,10 @@ export const injectIdentity = fp(
if (!authMode) return; if (!authMode) return;
const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined;
if (subOrganizationSelector) {
await slugSchema().parseAsync(subOrganizationSelector);
}
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { case AuthMode.JWT: {
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } = const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); await server.services.authToken.fnValidateJwtIdentity(token);
requestContext.set("orgId", orgId); requestContext.set("orgId", orgId);
requestContext.set("orgName", orgName); requestContext.set("orgName", orgName);
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" }); requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
@@ -180,11 +174,7 @@ export const injectIdentity = fp(
break; break;
} }
case AuthMode.IDENTITY_ACCESS_TOKEN: { case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken( const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
token,
req.realIp,
subOrganizationSelector
);
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId); requestContext.set("orgId", identity.orgId);
requestContext.set("orgName", identity.orgName); requestContext.set("orgName", identity.orgName);
@@ -223,9 +213,6 @@ export const injectIdentity = fp(
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,
rootOrgId: serviceToken.rootOrgId, rootOrgId: serviceToken.rootOrgId,
@@ -248,9 +235,6 @@ export const injectIdentity = fp(
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);
if (subOrganizationSelector)
throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` });
req.auth = { req.auth = {
authMode: AuthMode.SCIM_TOKEN, authMode: AuthMode.SCIM_TOKEN,
actor, actor,
+12 -10
View File
@@ -81,7 +81,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
token: z.string(), token: z.string(),
organizationId: z.string().optional() organizationId: z.string().optional(),
subOrganizationId: z.string().optional()
}) })
} }
}, },
@@ -89,14 +90,15 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig(); const appCfg = getConfig();
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME; let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
if (decodedToken.organizationId) { if (decodedToken.organizationId) {
const org = await server.services.org.findOrganizationById( const org = await server.services.org.findOrganizationById({
decodedToken.userId, userId: decodedToken.userId,
decodedToken.organizationId, orgId: decodedToken.subOrganizationId ? decodedToken.subOrganizationId : decodedToken.organizationId,
decodedToken.authMethod, actorAuthMethod: decodedToken.authMethod,
decodedToken.organizationId, actorOrgId: decodedToken.subOrganizationId ? decodedToken.subOrganizationId : decodedToken.organizationId,
decodedToken.organizationId rootOrgId: decodedToken.organizationId
); });
if (org && org.userTokenExpiration) { if (org && org.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
} }
@@ -110,14 +112,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
tokenVersionId: tokenVersion.id, tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion, accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId, organizationId: decodedToken.organizationId,
...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }),
isMfaVerified: decodedToken.isMfaVerified, isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod mfaMethod: decodedToken.mfaMethod
}, },
appCfg.AUTH_SECRET, appCfg.AUTH_SECRET,
{ expiresIn } { expiresIn }
); );
return { token, organizationId: decodedToken.organizationId, subOrganizationId: decodedToken.subOrganizationId };
return { token, organizationId: decodedToken.organizationId };
} }
}); });
}; };
@@ -316,13 +316,11 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
requestId: z.string().uuid() requestId: z.string().uuid()
}), }),
query: z.object({
projectId: z.string().uuid()
}),
response: { response: {
200: z.object({ 200: z.object({
status: z.nativeEnum(CertificateRequestStatus), status: z.nativeEnum(CertificateRequestStatus),
certificate: z.string().nullable(), certificate: z.string().nullable(),
certificateId: z.string().nullable(),
privateKey: z.string().nullable(), privateKey: z.string().nullable(),
serialNumber: z.string().nullable(), serialNumber: z.string().nullable(),
errorMessage: z.string().nullable(), errorMessage: z.string().nullable(),
@@ -333,18 +331,17 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const data = await server.services.certificateRequest.getCertificateFromRequest({ const { certificateRequest, projectId } = await server.services.certificateRequest.getCertificateFromRequest({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: (req.query as { projectId: string }).projectId,
certificateRequestId: req.params.requestId certificateRequestId: req.params.requestId
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: (req.query as { projectId: string }).projectId, projectId,
event: { event: {
type: EventType.GET_CERTIFICATE_REQUEST, type: EventType.GET_CERTIFICATE_REQUEST,
metadata: { metadata: {
@@ -352,7 +349,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
} }
} }
}); });
return data; return certificateRequest;
} }
}); });
@@ -5,6 +5,7 @@ import { IdentityAlicloudAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ALICLOUD_AUTH, ApiDocsTags } from "@app/lib/api-docs"; import { ALICLOUD_AUTH, ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -38,6 +39,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
message: "AccessKeyId must be alphanumeric" message: "AccessKeyId must be alphanumeric"
}) })
.describe(ALICLOUD_AUTH.LOGIN.AccessKeyId), .describe(ALICLOUD_AUTH.LOGIN.AccessKeyId),
subOrganizationName: slugSchema().optional().describe(ALICLOUD_AUTH.LOGIN.subOrganizationName),
SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod), SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod),
Timestamp: z Timestamp: z
.string() .string()
@@ -4,6 +4,7 @@ import { IdentityAwsAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, AWS_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, AWS_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -28,7 +29,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId),
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod), iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody), iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody),
iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders) iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders),
subOrganizationName: slugSchema().optional().describe(AWS_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -4,6 +4,7 @@ import { IdentityAzureAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, AZURE_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, AZURE_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -23,7 +24,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
description: "Login with Azure Auth for machine identity", description: "Login with Azure Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId),
jwt: z.string() jwt: z.string(),
subOrganizationName: slugSchema().optional().describe(AZURE_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -4,6 +4,7 @@ import { IdentityGcpAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, GCP_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, GCP_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -23,7 +24,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
description: "Login with GCP Auth for machine identity", description: "Login with GCP Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId),
jwt: z.string() jwt: z.string(),
subOrganizationName: slugSchema().optional().describe(GCP_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -4,6 +4,7 @@ import { IdentityJwtAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, JWT_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, JWT_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -99,7 +100,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
description: "Login with JWT Auth for machine identity", description: "Login with JWT Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId),
jwt: z.string().trim() jwt: z.string().trim(),
subOrganizationName: slugSchema().optional().describe(JWT_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -112,10 +114,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
}, },
handler: async (req) => { handler: async (req) => {
const { identityJwtAuth, accessToken, identityAccessToken, identity } = const { identityJwtAuth, accessToken, identityAccessToken, identity } =
await server.services.identityJwtAuth.login({ await server.services.identityJwtAuth.login(req.body);
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -44,7 +45,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
description: "Login with Kubernetes Auth for machine identity", description: "Login with Kubernetes Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId),
jwt: z.string().trim() jwt: z.string().trim(),
subOrganizationName: slugSchema().optional().describe(KUBERNETES_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -57,10 +59,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}, },
handler: async (req) => { handler: async (req) => {
const { identityKubernetesAuth, accessToken, identityAccessToken, identity } = const { identityKubernetesAuth, accessToken, identityAccessToken, identity } =
await server.services.identityKubernetesAuth.login({ await server.services.identityKubernetesAuth.login(req.body);
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -21,6 +21,7 @@ import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -124,7 +125,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
body: z.object({ body: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId),
username: z.string().describe(LDAP_AUTH.LOGIN.username), username: z.string().describe(LDAP_AUTH.LOGIN.username),
password: z.string().describe(LDAP_AUTH.LOGIN.password) password: z.string().describe(LDAP_AUTH.LOGIN.password),
subOrganizationName: slugSchema().optional().describe(LDAP_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -163,7 +165,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
const { identityId, user } = req.passportMachineIdentity; const { identityId, user } = req.passportMachineIdentity;
const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({ const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({
identityId identityId,
subOrganizationName: req.body.subOrganizationName
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -4,6 +4,7 @@ import { IdentityOciAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -40,7 +41,8 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
}); });
} }
}) })
.describe(OCI_AUTH.LOGIN.headers) .describe(OCI_AUTH.LOGIN.headers),
subOrganizationName: slugSchema().optional().describe(OCI_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -4,6 +4,7 @@ import { IdentityOidcAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OIDC_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, OIDC_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -47,7 +48,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
description: "Login with OIDC Auth for machine identity", description: "Login with OIDC Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId),
jwt: z.string().trim() jwt: z.string().trim(),
subOrganizationName: slugSchema().optional().describe(OIDC_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -60,10 +62,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
}, },
handler: async (req) => { handler: async (req) => {
const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } = const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } =
await server.services.identityOidcAuth.login({ await server.services.identityOidcAuth.login(req.body);
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -46,7 +47,8 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
tags: [ApiDocsTags.TlsCertAuth], tags: [ApiDocsTags.TlsCertAuth],
description: "Login with TLS Certificate Auth for machine identity", description: "Login with TLS Certificate Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId) identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId),
subOrganizationName: slugSchema().optional().describe(TLS_CERT_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -66,7 +68,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
const { identityTlsCertAuth, accessToken, identityAccessToken, identity } = const { identityTlsCertAuth, accessToken, identityAccessToken, identity } =
await server.services.identityTlsCertAuth.login({ await server.services.identityTlsCertAuth.login({
identityId: req.body.identityId, ...req.body,
clientCertificate: clientCertificate as string clientCertificate: clientCertificate as string
}); });
@@ -4,6 +4,7 @@ import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/sc
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, TOKEN_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, TOKEN_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -307,7 +308,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
identityId: z.string().describe(TOKEN_AUTH.CREATE_TOKEN.identityId) identityId: z.string().describe(TOKEN_AUTH.CREATE_TOKEN.identityId)
}), }),
body: z.object({ body: z.object({
name: z.string().optional().describe(TOKEN_AUTH.CREATE_TOKEN.name) name: z.string().optional().describe(TOKEN_AUTH.CREATE_TOKEN.name),
subOrganizationName: slugSchema().optional().describe(TOKEN_AUTH.CREATE_TOKEN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -4,6 +4,7 @@ import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@ap
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -35,7 +36,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
description: "Login with Universal Auth for machine identity", description: "Login with Universal Auth for machine identity",
body: z.object({ body: z.object({
clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId), clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId),
clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret) clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret),
subOrganizationName: slugSchema().optional().describe(UNIVERSAL_AUTH.LOGIN.subOrganizationName)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -55,7 +57,10 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
identity, identity,
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL accessTokenMaxTTL
} = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); } = await server.services.identityUa.login({
...req.body,
ip: req.realIp
});
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -60,26 +60,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
organization: sanitizedOrganizationSchema.extend({ organization: sanitizedOrganizationSchema
subOrganization: z
.object({
id: z.string(),
name: z.string()
})
.optional()
})
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const organization = await server.services.org.findOrganizationById( const organization = await server.services.org.findOrganizationById({
req.permission.id, userId: req.permission.id,
req.params.organizationId, orgId: req.params.organizationId,
req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
req.permission.rootOrgId, rootOrgId: req.permission.rootOrgId,
req.permission.orgId actorOrgId: req.permission.orgId
); });
return { organization }; return { organization };
} }
}); });
@@ -57,6 +57,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}, },
handler: async (req, res) => { handler: async (req, res) => {
const cfg = getConfig(); const cfg = getConfig();
const tokens = await server.services.login.selectOrganization({ const tokens = await server.services.login.selectOrganization({
userAgent: req.body.userAgent ?? req.headers["user-agent"], userAgent: req.body.userAgent ?? req.headers["user-agent"],
authJwtToken: req.headers.authorization, authJwtToken: req.headers.authorization,
@@ -196,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
}; };
// to parse jwt identity in inject identity plugin // to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => { const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const session = await tokenDAL.findOneTokenSession({ const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId, id: token.tokenVersionId,
userId: token.userId userId: token.userId
@@ -214,13 +214,17 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
let rootOrgId = ""; let rootOrgId = "";
let parentOrgId = ""; let parentOrgId = "";
if (token.organizationId) { if (token.organizationId) {
if (subOrganizationSelector) { // Check if token has sub-organization scope
if (token.subOrganizationId) {
const subOrganization = await orgDAL.findOne({ const subOrganization = await orgDAL.findOne({
rootOrgId: token.organizationId, id: token.subOrganizationId
slug: subOrganizationSelector
}); });
if (!subOrganization) if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); throw new BadRequestError({ message: `Sub organization ${token.subOrganizationId} not found` });
// Verify the sub-org belongs to the token's root organization
if (subOrganization.rootOrgId !== token.organizationId && subOrganization.id !== token.organizationId) {
throw new ForbiddenRequestError({ message: "Sub-organization does not belong to the token's organization" });
}
const orgMembership = await membershipUserDAL.findOne({ const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id, actorUserId: user.id,
+118 -41
View File
@@ -13,7 +13,13 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import {
BadRequestError,
DatabaseError,
ForbiddenRequestError,
NotFoundError,
UnauthorizedError
} from "@app/lib/errors";
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn"; import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
@@ -142,6 +148,7 @@ export const authLoginServiceFactory = ({
ip, ip,
userAgent, userAgent,
organizationId, organizationId,
subOrganizationId,
authMethod, authMethod,
isMfaVerified, isMfaVerified,
mfaMethod mfaMethod
@@ -150,6 +157,7 @@ export const authLoginServiceFactory = ({
ip: string; ip: string;
userAgent: string; userAgent: string;
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
authMethod: AuthMethod; authMethod: AuthMethod;
isMfaVerified?: boolean; isMfaVerified?: boolean;
mfaMethod?: MfaMethod; mfaMethod?: MfaMethod;
@@ -193,6 +201,7 @@ export const authLoginServiceFactory = ({
tokenVersionId: tokenSession.id, tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion, accessVersion: tokenSession.accessVersion,
organizationId, organizationId,
subOrganizationId,
isMfaVerified, isMfaVerified,
mfaMethod mfaMethod
}, },
@@ -208,6 +217,7 @@ export const authLoginServiceFactory = ({
tokenVersionId: tokenSession.id, tokenVersionId: tokenSession.id,
refreshVersion: tokenSession.refreshVersion, refreshVersion: tokenSession.refreshVersion,
organizationId, organizationId,
subOrganizationId,
isMfaVerified, isMfaVerified,
mfaMethod mfaMethod
}, },
@@ -526,33 +536,73 @@ export const authLoginServiceFactory = ({
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId); const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" }); if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
// Check if the user actually has access to the specified organization. // Check user membership in the sub-organization
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: organizationId,
scope: AccessScope.Organization,
status: OrgMembershipStatus.Accepted
});
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited"); if (!orgMembership) {
const selectedOrg = await orgDAL.findById(organizationId);
if (!selectedOrgMembership) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `User does not have access to the organization named ${selectedOrg?.name}` message: `User does not have access to the organization with ID ${organizationId}`
}); });
} }
// Check if authEnforced is true and the current auth method is not an enforced method const selectedOrg = await orgDAL.findById(organizationId);
if (!selectedOrg) {
throw new NotFoundError({ message: `Organization with ID '${organizationId}' not found` });
}
const isSubOrganization = Boolean(selectedOrg.rootOrgId && selectedOrg.id !== selectedOrg.rootOrgId);
const membershipRole = (await membershipRoleDAL.findOne({ membershipId: orgMembership.id })).role;
let rootOrg = selectedOrg;
if (isSubOrganization) {
if (!selectedOrg.rootOrgId) {
throw new BadRequestError({
message: "Invalid sub-organization"
});
}
rootOrg = await orgDAL.findById(selectedOrg.rootOrgId);
if (!rootOrg) {
throw new BadRequestError({
message: "Invalid sub-organization"
});
}
// Check user membership in the root organization
const rootOrgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: selectedOrg.rootOrgId,
scope: AccessScope.Organization,
status: OrgMembershipStatus.Accepted
});
if (!rootOrgMembership) {
throw new ForbiddenRequestError({
message: "User does not have access to the root organization"
});
}
}
if ( if (
selectedOrg.authEnforced && rootOrg.authEnforced &&
!isAuthMethodSaml(decodedToken.authMethod) && !isAuthMethodSaml(decodedToken.authMethod) &&
decodedToken.authMethod !== AuthMethod.OIDC && decodedToken.authMethod !== AuthMethod.OIDC &&
!(selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin) !(rootOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin)
) { ) {
throw new BadRequestError({ throw new BadRequestError({
message: "Login with the auth method required by your organization." message: "Login with the auth method required by your organization."
}); });
} }
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) { if (rootOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin; const canBypass = rootOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin;
if (!canBypass) { if (!canBypass) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -563,13 +613,13 @@ export const authLoginServiceFactory = ({
} }
if (decodedToken.authMethod === AuthMethod.GOOGLE) { if (decodedToken.authMethod === AuthMethod.GOOGLE) {
await orgDAL.updateById(selectedOrg.id, { await orgDAL.updateById(rootOrg.id, {
googleSsoAuthLastUsed: new Date() googleSsoAuthLastUsed: new Date()
}); });
} }
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled; const shouldCheckMfa = rootOrg.enforceMfa || user.isMfaEnabled;
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; const orgMfaMethod = rootOrg.enforceMfa ? (rootOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
const mfaMethod = orgMfaMethod ?? userMfaMethod; const mfaMethod = orgMfaMethod ?? userMfaMethod;
@@ -603,15 +653,16 @@ export const authLoginServiceFactory = ({
user, user,
userAgent, userAgent,
ip: ipAddress, ip: ipAddress,
organizationId, organizationId: isSubOrganization ? rootOrg.id : organizationId,
subOrganizationId: isSubOrganization ? organizationId : undefined,
isMfaVerified: decodedToken.isMfaVerified, isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod mfaMethod: decodedToken.mfaMethod
}); });
// In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced) // In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced)
if ( if (
selectedOrg.authEnforced && rootOrg.authEnforced &&
selectedOrg.bypassOrgAuthEnabled && rootOrg.bypassOrgAuthEnabled &&
!isAuthMethodSaml(decodedToken.authMethod) && !isAuthMethodSaml(decodedToken.authMethod) &&
decodedToken.authMethod !== AuthMethod.OIDC && decodedToken.authMethod !== AuthMethod.OIDC &&
decodedToken.authMethod !== AuthMethod.GOOGLE decodedToken.authMethod !== AuthMethod.GOOGLE
@@ -671,29 +722,55 @@ export const authLoginServiceFactory = ({
} }
} }
await auditLogService.createAuditLog({ // Create audit log for organization selection
orgId: organizationId, if (isSubOrganization) {
ipAddress, await auditLogService.createAuditLog({
userAgent, orgId: organizationId,
userAgentType: getUserAgentType(userAgent), ipAddress,
actor: { userAgent,
type: ActorType.USER, userAgentType: getUserAgentType(userAgent),
metadata: { actor: {
email: user.email, type: ActorType.USER,
userId: user.id, metadata: {
username: user.username, email: user.email,
authMethod: decodedToken.authMethod userId: user.id,
username: user.username,
authMethod: decodedToken.authMethod
}
},
event: {
type: EventType.SELECT_SUB_ORGANIZATION,
metadata: {
organizationId,
organizationName: selectedOrg.name,
rootOrganizationId: selectedOrg.rootOrgId || ""
}
} }
}, });
event: { } else {
type: EventType.SELECT_ORGANIZATION, await auditLogService.createAuditLog({
metadata: { orgId: organizationId,
organizationId, ipAddress,
organizationName: selectedOrg.name userAgent,
userAgentType: getUserAgentType(userAgent),
actor: {
type: ActorType.USER,
metadata: {
email: user.email,
userId: user.id,
username: user.username,
authMethod: decodedToken.authMethod
}
},
event: {
type: EventType.SELECT_ORGANIZATION,
metadata: {
organizationId,
organizationName: selectedOrg.name
}
} }
} });
}); }
return { return {
...tokens, ...tokens,
user, user,
@@ -258,13 +258,13 @@ export const authSignupServiceFactory = ({
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
if (organizationId) { if (organizationId) {
const org = await orgService.findOrganizationById( const org = await orgService.findOrganizationById({
user.id, userId: user.id,
organizationId, orgId: organizationId,
authMethod, actorAuthMethod: authMethod,
organizationId, actorOrgId: organizationId,
organizationId rootOrgId: organizationId
); });
if (org && org.userTokenExpiration) { if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = org.userTokenExpiration; refreshTokenExpiresIn = org.userTokenExpiration;
+2
View File
@@ -55,6 +55,7 @@ export type AuthModeJwtTokenPayload = {
tokenVersionId: string; tokenVersionId: string;
accessVersion: number; accessVersion: number;
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
isMfaVerified?: boolean; isMfaVerified?: boolean;
mfaMethod?: MfaMethod; mfaMethod?: MfaMethod;
}; };
@@ -74,6 +75,7 @@ export type AuthModeRefreshJwtTokenPayload = {
tokenVersionId: string; tokenVersionId: string;
refreshVersion: number; refreshVersion: number;
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
isMfaVerified?: boolean; isMfaVerified?: boolean;
mfaMethod?: MfaMethod; mfaMethod?: MfaMethod;
}; };
@@ -258,7 +258,7 @@ describe("CertificateRequestService", () => {
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
(mockCertificateService.getCertPrivateKey as any).mockResolvedValue(mockPrivateKey); (mockCertificateService.getCertPrivateKey as any).mockResolvedValue(mockPrivateKey);
const result = await service.getCertificateFromRequest(mockGetData); const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData);
expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith(
"550e8400-e29b-41d4-a716-446655440005" "550e8400-e29b-41d4-a716-446655440005"
@@ -277,8 +277,9 @@ describe("CertificateRequestService", () => {
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002" actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
}); });
expect(result).toEqual({ expect(certificateRequest).toEqual({
status: CertificateRequestStatus.ISSUED, status: CertificateRequestStatus.ISSUED,
certificateId: "550e8400-e29b-41d4-a716-446655440006",
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----",
privateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----", privateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----",
serialNumber: "123456", serialNumber: "123456",
@@ -286,6 +287,7 @@ describe("CertificateRequestService", () => {
createdAt: mockRequestWithCert.createdAt, createdAt: mockRequestWithCert.createdAt,
updatedAt: mockRequestWithCert.updatedAt updatedAt: mockRequestWithCert.updatedAt
}); });
expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003");
}); });
it("should get certificate from request successfully when no certificate is attached", async () => { it("should get certificate from request successfully when no certificate is attached", async () => {
@@ -310,10 +312,11 @@ describe("CertificateRequestService", () => {
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithoutCert); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithoutCert);
const result = await service.getCertificateFromRequest(mockGetData); const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData);
expect(result).toEqual({ expect(certificateRequest).toEqual({
status: CertificateRequestStatus.PENDING, status: CertificateRequestStatus.PENDING,
certificateId: null,
certificate: null, certificate: null,
privateKey: null, privateKey: null,
serialNumber: null, serialNumber: null,
@@ -321,6 +324,7 @@ describe("CertificateRequestService", () => {
createdAt: mockRequestWithoutCert.createdAt, createdAt: mockRequestWithoutCert.createdAt,
updatedAt: mockRequestWithoutCert.updatedAt updatedAt: mockRequestWithoutCert.updatedAt
}); });
expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003");
}); });
it("should get certificate from request successfully when user lacks private key permission", async () => { it("should get certificate from request successfully when user lacks private key permission", async () => {
@@ -354,7 +358,7 @@ describe("CertificateRequestService", () => {
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert);
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
const result = await service.getCertificateFromRequest(mockGetData); const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData);
expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith(
"550e8400-e29b-41d4-a716-446655440005" "550e8400-e29b-41d4-a716-446655440005"
@@ -367,8 +371,9 @@ describe("CertificateRequestService", () => {
actorOrgId: "550e8400-e29b-41d4-a716-446655440002" actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
}); });
expect(mockCertificateService.getCertPrivateKey).not.toHaveBeenCalled(); expect(mockCertificateService.getCertPrivateKey).not.toHaveBeenCalled();
expect(result).toEqual({ expect(certificateRequest).toEqual({
status: CertificateRequestStatus.ISSUED, status: CertificateRequestStatus.ISSUED,
certificateId: "550e8400-e29b-41d4-a716-446655440008",
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----",
privateKey: null, privateKey: null,
serialNumber: "123456", serialNumber: "123456",
@@ -376,6 +381,7 @@ describe("CertificateRequestService", () => {
createdAt: mockRequestWithCert.createdAt, createdAt: mockRequestWithCert.createdAt,
updatedAt: mockRequestWithCert.updatedAt updatedAt: mockRequestWithCert.updatedAt
}); });
expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003");
}); });
it("should get certificate from request successfully when user has private key permission but key retrieval fails", async () => { it("should get certificate from request successfully when user has private key permission but key retrieval fails", async () => {
@@ -414,7 +420,7 @@ describe("CertificateRequestService", () => {
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
(mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key not found")); (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key not found"));
const result = await service.getCertificateFromRequest(mockGetData); const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData);
expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith(
"550e8400-e29b-41d4-a716-446655440005" "550e8400-e29b-41d4-a716-446655440005"
@@ -433,8 +439,9 @@ describe("CertificateRequestService", () => {
actorAuthMethod: AuthMethod.EMAIL, actorAuthMethod: AuthMethod.EMAIL,
actorOrgId: "550e8400-e29b-41d4-a716-446655440002" actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
}); });
expect(result).toEqual({ expect(certificateRequest).toEqual({
status: CertificateRequestStatus.ISSUED, status: CertificateRequestStatus.ISSUED,
certificateId: "550e8400-e29b-41d4-a716-446655440009",
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----",
privateKey: null, privateKey: null,
serialNumber: "123456", serialNumber: "123456",
@@ -442,6 +449,7 @@ describe("CertificateRequestService", () => {
createdAt: mockRequestWithCert.createdAt, createdAt: mockRequestWithCert.createdAt,
updatedAt: mockRequestWithCert.updatedAt updatedAt: mockRequestWithCert.updatedAt
}); });
expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003");
}); });
it("should get certificate from request with error message when failed", async () => { it("should get certificate from request with error message when failed", async () => {
@@ -466,17 +474,19 @@ describe("CertificateRequestService", () => {
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockFailedRequest); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockFailedRequest);
const result = await service.getCertificateFromRequest(mockGetData); const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData);
expect(result).toEqual({ expect(certificateRequest).toEqual({
status: CertificateRequestStatus.FAILED, status: CertificateRequestStatus.FAILED,
certificate: null, certificate: null,
certificateId: null,
privateKey: null, privateKey: null,
serialNumber: null, serialNumber: null,
errorMessage: "Certificate issuance failed", errorMessage: "Certificate issuance failed",
createdAt: mockFailedRequest.createdAt, createdAt: mockFailedRequest.createdAt,
updatedAt: mockFailedRequest.updatedAt updatedAt: mockFailedRequest.updatedAt
}); });
expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003");
}); });
it("should throw NotFoundError when certificate request does not exist", async () => { it("should throw NotFoundError when certificate request does not exist", async () => {
@@ -170,13 +170,17 @@ export const certificateRequestServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
projectId,
certificateRequestId certificateRequestId
}: TGetCertificateFromRequestDTO) => { }: TGetCertificateFromRequestDTO) => {
const certificateRequest = await certificateRequestDAL.findByIdWithCertificate(certificateRequestId);
if (!certificateRequest) {
throw new NotFoundError({ message: "Certificate request not found" });
}
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId: certificateRequest.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.CertificateManager actionProjectType: ActionProjectType.CertificateManager
@@ -187,25 +191,20 @@ export const certificateRequestServiceFactory = ({
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
const certificateRequest = await certificateRequestDAL.findByIdWithCertificate(certificateRequestId);
if (!certificateRequest) {
throw new NotFoundError({ message: "Certificate request not found" });
}
if (certificateRequest.projectId !== projectId) {
throw new NotFoundError({ message: "Certificate request not found" });
}
// If no certificate is attached, return basic info // If no certificate is attached, return basic info
if (!certificateRequest.certificate) { if (!certificateRequest.certificate) {
return { return {
status: certificateRequest.status as CertificateRequestStatus, certificateRequest: {
certificate: null, status: certificateRequest.status as CertificateRequestStatus,
privateKey: null, certificate: null,
serialNumber: null, certificateId: null,
errorMessage: certificateRequest.errorMessage || null, privateKey: null,
createdAt: certificateRequest.createdAt, serialNumber: null,
updatedAt: certificateRequest.updatedAt errorMessage: certificateRequest.errorMessage || null,
createdAt: certificateRequest.createdAt,
updatedAt: certificateRequest.updatedAt
},
projectId: certificateRequest.projectId
}; };
} }
@@ -240,13 +239,17 @@ export const certificateRequestServiceFactory = ({
} }
return { return {
status: certificateRequest.status as CertificateRequestStatus, certificateRequest: {
certificate: certBody.certificate, status: certificateRequest.status as CertificateRequestStatus,
privateKey, certificate: certBody.certificate,
serialNumber: certificateRequest.certificate.serialNumber, certificateId: certificateRequest.certificate.id,
errorMessage: certificateRequest.errorMessage || null, privateKey,
createdAt: certificateRequest.createdAt, serialNumber: certificateRequest.certificate.serialNumber,
updatedAt: certificateRequest.updatedAt errorMessage: certificateRequest.errorMessage || null,
createdAt: certificateRequest.createdAt,
updatedAt: certificateRequest.updatedAt
},
projectId: certificateRequest.projectId
}; };
}; };
@@ -27,7 +27,7 @@ export type TGetCertificateRequestDTO = TProjectPermission & {
certificateRequestId: string; certificateRequestId: string;
}; };
export type TGetCertificateFromRequestDTO = TProjectPermission & { export type TGetCertificateFromRequestDTO = Omit<TProjectPermission, "projectId"> & {
certificateRequestId: string; certificateRequestId: string;
}; };
@@ -18,7 +18,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.where(filter) .where(filter)
.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("orgId").withSchema(TableName.Identity).as("identityScopeOrgId")) .select(db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"))
.select(db.ref("subOrganizationId").withSchema(TableName.IdentityAccessToken).as("subOrganizationId"))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName")) .select(db.ref("name").withSchema(TableName.Identity).as("identityName"))
.first(); .first();
@@ -184,11 +184,7 @@ export const identityAccessTokenServiceFactory = ({
return { revokedToken }; return { revokedToken };
}; };
const fnValidateIdentityAccessToken = async ( const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => {
token: TIdentityAccessTokenJwtPayload,
ipAddress?: string,
subOrganizationSelector?: 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
@@ -209,46 +205,30 @@ export const identityAccessTokenServiceFactory = ({
trustedIps: trustedIps as TIp[] trustedIps: trustedIps as TIp[]
}); });
} }
let orgId = "";
let orgName = "";
let parentOrgId = "";
const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId });
const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id;
if (subOrganizationSelector) { const scopeOrgId = identityAccessToken.subOrganizationId || identityAccessToken.identityOrgId;
const subOrganization = await orgDAL.findOne({ rootOrgId, slug: subOrganizationSelector });
if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
const identityOrgMembership = await membershipIdentityDAL.findOne({ const identityOrgDetails = await orgDAL.findOne({ id: scopeOrgId });
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
scopeOrgId: subOrganization.id
});
if (!identityOrgMembership) { const isSubOrg = Boolean(identityOrgDetails.rootOrgId);
throw new BadRequestError({ message: "Identity does not belong to this organization" });
}
orgId = subOrganization.id;
orgName = subOrganization.name;
parentOrgId = subOrganization.parentOrgId as string; const rootOrgId = isSubOrg ? identityOrgDetails.rootOrgId || identityOrgDetails.id : identityOrgDetails.id;
} else {
const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
scopeOrgId: identityOrgDetails.id
});
if (!identityOrgMembership) { // Verify identity membership in the organization
throw new BadRequestError({ message: "Identity does not belong to this organization" }); const identityOrgMembership = await membershipIdentityDAL.findOne({
} scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
scopeOrgId: identityOrgDetails.id
});
orgId = identityOrgDetails.id; if (!identityOrgMembership) {
orgName = identityOrgDetails.name; throw new BadRequestError({ message: "Identity does not belong to this organization" });
parentOrgId = rootOrgId;
} }
const orgId = identityOrgDetails.id;
const orgName = identityOrgDetails.name;
const parentOrgId = identityOrgDetails.parentOrgId || rootOrgId;
let { accessTokenNumUses } = identityAccessToken; let { accessTokenNumUses } = identityAccessToken;
const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id); const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id);
if (tokenStatusInCache) { if (tokenStatusInCache) {
@@ -53,7 +53,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = {
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">; membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityAliCloudAuthServiceFactory = ReturnType<typeof identityAliCloudAuthServiceFactory>; export type TIdentityAliCloudAuthServiceFactory = ReturnType<typeof identityAliCloudAuthServiceFactory>;
@@ -67,7 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({
permissionService, permissionService,
orgDAL orgDAL
}: TIdentityAliCloudAuthServiceFactoryDep) => { }: TIdentityAliCloudAuthServiceFactoryDep) => {
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { const login = async ({ identityId, subOrganizationName, ...params }: TLoginAliCloudAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
if (!identityAliCloudAuth) { if (!identityAliCloudAuth) {
@@ -80,6 +80,10 @@ export const identityAliCloudAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const requestUrl = new URL("https://sts.aliyuncs.com"); const requestUrl = new URL("https://sts.aliyuncs.com");
@@ -103,6 +107,30 @@ export const identityAliCloudAuthServiceFactory = ({
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
// Generate the token // Generate the token
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
@@ -132,7 +160,8 @@ export const identityAliCloudAuthServiceFactory = ({
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.ALICLOUD_AUTH authMethod: IdentityAuthMethod.ALICLOUD_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -11,6 +11,7 @@ export type TLoginAliCloudAuthDTO = {
SignatureVersion: string; SignatureVersion: string;
SignatureNonce: string; SignatureNonce: string;
Signature: string; Signature: string;
subOrganizationName?: string;
}; };
export type TAttachAliCloudAuthDTO = { export type TAttachAliCloudAuthDTO = {
@@ -53,7 +53,7 @@ type TIdentityAwsAuthServiceFactoryDep = {
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">; membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityAwsAuthServiceFactory = ReturnType<typeof identityAwsAuthServiceFactory>; export type TIdentityAwsAuthServiceFactory = ReturnType<typeof identityAwsAuthServiceFactory>;
@@ -101,7 +101,13 @@ export const identityAwsAuthServiceFactory = ({
permissionService, permissionService,
orgDAL orgDAL
}: TIdentityAwsAuthServiceFactoryDep) => { }: TIdentityAwsAuthServiceFactoryDep) => {
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => { const login = async ({
identityId,
iamHttpRequestMethod,
iamRequestBody,
iamRequestHeaders,
subOrganizationName
}: TLoginAwsAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
if (!identityAwsAuth) { if (!identityAwsAuth) {
@@ -112,6 +118,11 @@ export const identityAwsAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString(); const body: string = Buffer.from(iamRequestBody, "base64").toString();
@@ -179,6 +190,30 @@ export const identityAwsAuthServiceFactory = ({
} }
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -207,7 +242,8 @@ export const identityAwsAuthServiceFactory = ({
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AWS_AUTH authMethod: IdentityAuthMethod.AWS_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -5,6 +5,7 @@ export type TLoginAwsAuthDTO = {
iamHttpRequestMethod: string; iamHttpRequestMethod: string;
iamRequestBody: string; iamRequestBody: string;
iamRequestHeaders: string; iamRequestHeaders: string;
subOrganizationName?: string;
}; };
export type TAttachAwsAuthDTO = { export type TAttachAwsAuthDTO = {
@@ -49,7 +49,7 @@ type TIdentityAzureAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityAzureAuthServiceFactory = ReturnType<typeof identityAzureAuthServiceFactory>; export type TIdentityAzureAuthServiceFactory = ReturnType<typeof identityAzureAuthServiceFactory>;
@@ -63,7 +63,7 @@ export const identityAzureAuthServiceFactory = ({
licenseService, licenseService,
orgDAL orgDAL
}: TIdentityAzureAuthServiceFactoryDep) => { }: TIdentityAzureAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => { const login = async ({ identityId, jwt: azureJwt, subOrganizationName }: TLoginAzureAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
if (!identityAzureAuth) { if (!identityAzureAuth) {
@@ -74,6 +74,10 @@ export const identityAzureAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const azureIdentity = await validateAzureIdentity({ const azureIdentity = await validateAzureIdentity({
@@ -98,6 +102,30 @@ export const identityAzureAuthServiceFactory = ({
} }
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -126,7 +154,8 @@ export const identityAzureAuthServiceFactory = ({
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AZURE_AUTH authMethod: IdentityAuthMethod.AZURE_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TLoginAzureAuthDTO = { export type TLoginAzureAuthDTO = {
identityId: string; identityId: string;
jwt: string; jwt: string;
subOrganizationName?: string;
}; };
export type TAttachAzureAuthDTO = { export type TAttachAzureAuthDTO = {
@@ -47,7 +47,7 @@ type TIdentityGcpAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityGcpAuthServiceFactory = ReturnType<typeof identityGcpAuthServiceFactory>; export type TIdentityGcpAuthServiceFactory = ReturnType<typeof identityGcpAuthServiceFactory>;
@@ -61,7 +61,7 @@ export const identityGcpAuthServiceFactory = ({
licenseService, licenseService,
orgDAL orgDAL
}: TIdentityGcpAuthServiceFactoryDep) => { }: TIdentityGcpAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => { const login = async ({ identityId, jwt: gcpJwt, subOrganizationName }: TLoginGcpAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (!identityGcpAuth) { if (!identityGcpAuth) {
@@ -72,6 +72,11 @@ export const identityGcpAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
let gcpIdentityDetails: TGcpIdentityDetails; let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) { switch (identityGcpAuth.type) {
@@ -138,6 +143,30 @@ export const identityGcpAuthServiceFactory = ({
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -166,7 +195,8 @@ export const identityGcpAuthServiceFactory = ({
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.GCP_AUTH authMethod: IdentityAuthMethod.GCP_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TLoginGcpAuthDTO = { export type TLoginGcpAuthDTO = {
identityId: string; identityId: string;
jwt: string; jwt: string;
subOrganizationName?: string;
}; };
export type TAttachGcpAuthDTO = { export type TAttachGcpAuthDTO = {
@@ -60,7 +60,7 @@ type TIdentityJwtAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityJwtAuthServiceFactory = ReturnType<typeof identityJwtAuthServiceFactory>; export type TIdentityJwtAuthServiceFactory = ReturnType<typeof identityJwtAuthServiceFactory>;
@@ -75,7 +75,7 @@ export const identityJwtAuthServiceFactory = ({
kmsService, kmsService,
orgDAL orgDAL
}: TIdentityJwtAuthServiceFactoryDep) => { }: TIdentityJwtAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { const login = async ({ identityId, jwt: jwtValue, subOrganizationName }: TLoginJwtAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
if (!identityJwtAuth) { if (!identityJwtAuth) {
@@ -86,6 +86,11 @@ export const identityJwtAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
@@ -218,6 +223,30 @@ export const identityJwtAuthServiceFactory = ({
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -246,7 +275,8 @@ export const identityJwtAuthServiceFactory = ({
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.JWT_AUTH authMethod: IdentityAuthMethod.JWT_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -49,4 +49,5 @@ export type TRevokeJwtAuthDTO = {
export type TLoginJwtAuthDTO = { export type TLoginJwtAuthDTO = {
identityId: string; identityId: string;
jwt: string; jwt: string;
subOrganizationName?: string;
}; };
@@ -78,7 +78,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
gatewayV2Service: TGatewayV2ServiceFactory; gatewayV2Service: TGatewayV2ServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">; gatewayDAL: Pick<TGatewayDALFactory, "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">; gatewayV2DAL: Pick<TGatewayV2DALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>; export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
@@ -185,7 +185,7 @@ export const identityKubernetesAuthServiceFactory = ({
return callbackResult; return callbackResult;
}; };
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const login = async ({ identityId, jwt: serviceAccountJwt, subOrganizationName }: TLoginKubernetesAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) { if (!identityKubernetesAuth) {
@@ -198,6 +198,10 @@ export const identityKubernetesAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
@@ -459,6 +463,30 @@ export const identityKubernetesAuthServiceFactory = ({
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -487,7 +515,8 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.KUBERNETES_AUTH authMethod: IdentityAuthMethod.KUBERNETES_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TLoginKubernetesAuthDTO = { export type TLoginKubernetesAuthDTO = {
identityId: string; identityId: string;
jwt: string; jwt: string;
subOrganizationName?: string;
}; };
export enum IdentityKubernetesAuthTokenReviewMode { export enum IdentityKubernetesAuthTokenReviewMode {
@@ -70,7 +70,7 @@ type TIdentityLdapAuthServiceFactoryDep = {
TKeyStoreFactory, TKeyStoreFactory,
"setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock"
>; >;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>; export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
@@ -153,7 +153,7 @@ export const identityLdapAuthServiceFactory = ({
return { opts, ldapConfig }; return { opts, ldapConfig };
}; };
const login = async ({ identityId }: TLoginLdapAuthDTO) => { const login = async ({ identityId, subOrganizationName }: TLoginLdapAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
@@ -167,6 +167,11 @@ export const identityLdapAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
const plan = await licenseService.getPlan(identity.orgId); const plan = await licenseService.getPlan(identity.orgId);
if (!plan.ldap) { if (!plan.ldap) {
throw new BadRequestError({ throw new BadRequestError({
@@ -174,6 +179,29 @@ export const identityLdapAuthServiceFactory = ({
"Failed to login to identity due to plan restriction. Upgrade plan to login to use LDAP authentication." "Failed to login to identity due to plan restriction. Upgrade plan to login to use LDAP authentication."
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
try { try {
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
@@ -204,7 +232,8 @@ export const identityLdapAuthServiceFactory = ({
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.LDAP_AUTH authMethod: IdentityAuthMethod.LDAP_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -59,6 +59,7 @@ export type TGetLdapAuthDTO = {
export type TLoginLdapAuthDTO = { export type TLoginLdapAuthDTO = {
identityId: string; identityId: string;
subOrganizationName?: string;
}; };
export type TRevokeLdapAuthDTO = { export type TRevokeLdapAuthDTO = {
@@ -51,7 +51,7 @@ type TIdentityOciAuthServiceFactoryDep = {
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">; membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne" | "update" | "getIdentityById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityOciAuthServiceFactory = ReturnType<typeof identityOciAuthServiceFactory>; export type TIdentityOciAuthServiceFactory = ReturnType<typeof identityOciAuthServiceFactory>;
@@ -65,7 +65,7 @@ export const identityOciAuthServiceFactory = ({
permissionService, permissionService,
orgDAL orgDAL
}: TIdentityOciAuthServiceFactoryDep) => { }: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { const login = async ({ identityId, headers, userOcid, subOrganizationName }: TLoginOciAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) { if (!identityOciAuth) {
@@ -76,6 +76,11 @@ export const identityOciAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format. // Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
@@ -108,6 +113,30 @@ export const identityOciAuthServiceFactory = ({
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
// Generate the token // Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
@@ -137,7 +166,8 @@ export const identityOciAuthServiceFactory = ({
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OCI_AUTH authMethod: IdentityAuthMethod.OCI_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -9,6 +9,7 @@ export type TLoginOciAuthDTO = {
"x-date"?: string; "x-date"?: string;
date?: string; date?: string;
}; };
subOrganizationName?: string;
}; };
export type TAttachOciAuthDTO = { export type TAttachOciAuthDTO = {
@@ -61,7 +61,7 @@ type TIdentityOidcAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuthServiceFactory>; export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuthServiceFactory>;
@@ -76,7 +76,7 @@ export const identityOidcAuthServiceFactory = ({
kmsService, kmsService,
orgDAL orgDAL
}: TIdentityOidcAuthServiceFactoryDep) => { }: TIdentityOidcAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const login = async ({ identityId, jwt: oidcJwt, subOrganizationName }: TLoginOidcAuthDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) { if (!identityOidcAuth) {
@@ -87,6 +87,11 @@ export const identityOidcAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
@@ -286,6 +291,30 @@ export const identityOidcAuthServiceFactory = ({
}); });
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -314,7 +343,8 @@ export const identityOidcAuthServiceFactory = ({
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OIDC_AUTH authMethod: IdentityAuthMethod.OIDC_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -38,6 +38,7 @@ export type TGetOidcAuthDTO = {
export type TLoginOidcAuthDTO = { export type TLoginOidcAuthDTO = {
identityId: string; identityId: string;
jwt: string; jwt: string;
subOrganizationName?: string;
}; };
export type TRevokeOidcAuthDTO = { export type TRevokeOidcAuthDTO = {
@@ -46,7 +46,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
const parseSubjectDetails = (data: string) => { const parseSubjectDetails = (data: string) => {
@@ -68,7 +68,11 @@ export const identityTlsCertAuthServiceFactory = ({
kmsService, kmsService,
orgDAL orgDAL
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => { }: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => { const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({
identityId,
clientCertificate,
subOrganizationName
}) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId }); const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
if (!identityTlsCertAuth) { if (!identityTlsCertAuth) {
@@ -81,6 +85,10 @@ export const identityTlsCertAuthServiceFactory = ({
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
@@ -128,6 +136,30 @@ export const identityTlsCertAuthServiceFactory = ({
} }
} }
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
// Generate the token // Generate the token
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
@@ -157,7 +189,8 @@ export const identityTlsCertAuthServiceFactory = ({
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.TLS_CERT_AUTH authMethod: IdentityAuthMethod.TLS_CERT_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -4,6 +4,7 @@ import { TProjectPermission } from "@app/lib/types";
export type TLoginTlsCertAuthDTO = { export type TLoginTlsCertAuthDTO = {
identityId: string; identityId: string;
clientCertificate: string; clientCertificate: string;
subOrganizationName?: string;
}; };
export type TAttachTlsCertAuthDTO = { export type TAttachTlsCertAuthDTO = {
@@ -59,7 +59,7 @@ type TIdentityTokenAuthServiceFactoryDep = {
>; >;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
}; };
export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAuthServiceFactory>; export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAuthServiceFactory>;
@@ -424,7 +424,8 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
name, name,
isActorSuperAdmin isActorSuperAdmin,
subOrganizationName
}: TCreateTokenAuthTokenDTO) => { }: TCreateTokenAuthTokenDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
@@ -503,6 +504,36 @@ export const identityTokenAuthServiceFactory = ({
const identity = await identityDAL.findById(identityTokenAuth.identityId); const identity = await identityDAL.findById(identityTokenAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
identity.projectId identity.projectId
@@ -529,7 +560,8 @@ export const identityTokenAuthServiceFactory = ({
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit,
name, name,
authMethod: IdentityAuthMethod.TOKEN_AUTH authMethod: IdentityAuthMethod.TOKEN_AUTH,
subOrganizationId
}, },
tx tx
); );
@@ -30,6 +30,7 @@ export type TRevokeTokenAuthDTO = {
export type TCreateTokenAuthTokenDTO = { export type TCreateTokenAuthTokenDTO = {
identityId: string; identityId: string;
name?: string; name?: string;
subOrganizationName?: string;
isActorSuperAdmin?: boolean; isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -41,6 +41,7 @@ import {
TGetUaClientSecretsDTO, TGetUaClientSecretsDTO,
TGetUaDTO, TGetUaDTO,
TGetUniversalAuthClientSecretByIdDTO, TGetUniversalAuthClientSecretByIdDTO,
TLoginUaDTO,
TRevokeUaClientSecretDTO, TRevokeUaClientSecretDTO,
TRevokeUaDTO, TRevokeUaDTO,
TUpdateUaDTO TUpdateUaDTO
@@ -54,7 +55,7 @@ type TIdentityUaServiceFactoryDep = {
membershipIdentityDAL: TMembershipIdentityDALFactory; membershipIdentityDAL: TMembershipIdentityDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById" | "findOne">;
keyStore: Pick< keyStore: Pick<
TKeyStoreFactory, TKeyStoreFactory,
"setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock"
@@ -79,7 +80,7 @@ export const identityUaServiceFactory = ({
keyStore, keyStore,
identityDAL identityDAL
}: TIdentityUaServiceFactoryDep) => { }: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string, ip: string) => { const login = async ({ clientId, clientSecret, ip, subOrganizationName }: TLoginUaDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const identityUa = await identityUaDAL.findOne({ clientId }); const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) { if (!identityUa) {
@@ -90,6 +91,10 @@ export const identityUaServiceFactory = ({
const identity = await identityDAL.findById(identityUa.identityId); const identity = await identityDAL.findById(identityUa.identityId);
const org = await orgDAL.findById(identity.orgId); const org = await orgDAL.findById(identity.orgId);
const isSubOrgIdentity = Boolean(org.rootOrgId);
// If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified
let subOrganizationId = isSubOrgIdentity ? org.id : null;
try { try {
checkIPAgainstBlocklist({ checkIPAgainstBlocklist({
@@ -229,6 +234,30 @@ export const identityUaServiceFactory = ({
accessTokenMaxTTL: 1000000000 accessTokenMaxTTL: 1000000000
}; };
if (subOrganizationName) {
if (!isSubOrgIdentity) {
const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName });
if (!subOrg) {
throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` });
}
const subOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identity.id,
scopeOrgId: subOrg.id
});
if (!subOrgMembership) {
throw new UnauthorizedError({
message: `Identity not authorized to access sub organization ${subOrganizationName}`
});
}
subOrganizationId = subOrg.id;
}
}
const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
@@ -259,6 +288,7 @@ export const identityUaServiceFactory = ({
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
accessTokenPeriod: identityUa.accessTokenPeriod, accessTokenPeriod: identityUa.accessTokenPeriod,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
subOrganizationId,
...accessTokenTTLParams ...accessTokenTTLParams
}, },
tx tx
@@ -1,5 +1,12 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TLoginUaDTO = {
clientId: string;
clientSecret: string;
ip: string;
subOrganizationName?: string;
};
export type TAttachUaDTO = { export type TAttachUaDTO = {
identityId: string; identityId: string;
accessTokenTTL: number; accessTokenTTL: number;
@@ -188,16 +188,29 @@ export const membershipUserServiceFactory = ({
}); });
if (existingMemberships.length === users.length) return { memberships: [] }; if (existingMemberships.length === users.length) return { memberships: [] };
const orgDetails = await orgDAL.findById(dto.permission.orgId);
const isSubOrganization = Boolean(orgDetails.rootOrgId);
const newMembershipUsers = users.filter((user) => !existingMemberships?.find((el) => el.actorUserId === user.id)); const newMembershipUsers = users.filter((user) => !existingMemberships?.find((el) => el.actorUserId === user.id));
await factory.onCreateMembershipUserGuard(dto, newMembershipUsers); await factory.onCreateMembershipUserGuard(dto, newMembershipUsers);
const newMemberships = newMembershipUsers.map((user) => ({ const newMemberships = newMembershipUsers.map((user) => {
scope: scopeData.scope, let status: OrgMembershipStatus | undefined;
...scopeDatabaseFields, if (scopeData.scope === AccessScope.Organization) {
actorUserId: user.id, if (isSubOrganization) {
status: scopeData.scope === AccessScope.Organization ? OrgMembershipStatus.Invited : undefined, status = OrgMembershipStatus.Accepted;
inviteEmail: scopeData.scope === AccessScope.Organization ? user.email : undefined } else {
})); status = OrgMembershipStatus.Invited;
}
}
return {
scope: scopeData.scope,
...scopeDatabaseFields,
actorUserId: user.id,
status,
inviteEmail: status === OrgMembershipStatus.Invited ? user.email : undefined
};
});
const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role));
const hasCustomRole = customInputRoles.length > 0; const hasCustomRole = customInputRoles.length > 0;
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, OrganizationActionScope, OrgMembershipStatus } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
@@ -122,16 +122,33 @@ export const newOrgMembershipUserFactory = ({
const signUpTokens: { email: string; link: string }[] = []; const signUpTokens: { email: string; link: string }[] = [];
const orgDetails = await orgDAL.findById(dto.permission.orgId); const orgDetails = await orgDAL.findById(dto.permission.orgId);
if (orgDetails.rootOrgId) { if (orgDetails.rootOrgId) {
const emails = newUsers.map((el) => el.email).filter(Boolean); // checking if the users have accepted the invitation in the root organization to send the email
await smtpService.sendMail({ const orgMembershipAccepted = await membershipUserDAL.find({
template: SmtpTemplates.SubOrgInvite, scope: AccessScope.Organization,
subjectLine: "Infisical sub-organization invitation", scopeOrgId: orgDetails.rootOrgId,
recipients: emails as string[], status: OrgMembershipStatus.Accepted,
substitutions: { $in: {
subOrganizationName: orgDetails.slug, actorUserId: newUsers.map((el) => el.id)
callback_url: `${appCfg.SITE_URL}/organizations/${dto.permission.orgId}/projects?subOrganization=${orgDetails.slug}`
} }
}); });
const orgMembershipAcceptedUserIds = orgMembershipAccepted.map((el) => el.actorUserId as string);
const emails = newUsers
.filter((el) => Boolean(el?.email) && orgMembershipAcceptedUserIds.includes(el.id))
.map((el) => el?.email as string);
if (emails.length) {
await smtpService.sendMail({
template: SmtpTemplates.SubOrgInvite,
subjectLine: "Infisical sub-organization invitation",
recipients: emails,
substitutions: {
subOrganizationName: orgDetails.slug,
callback_url: `${appCfg.SITE_URL}/organizations/${dto.permission.orgId}/projects`
}
});
}
} else { } else {
await Promise.allSettled( await Promise.allSettled(
newUsers.map(async (el) => { newUsers.map(async (el) => {
+3 -1
View File
@@ -28,5 +28,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
shareSecretsProductEnabled: true, shareSecretsProductEnabled: true,
maxSharedSecretLifetime: true, maxSharedSecretLifetime: true,
maxSharedSecretViewLimit: true, maxSharedSecretViewLimit: true,
blockDuplicateSecretSyncDestinations: true blockDuplicateSecretSyncDestinations: true,
rootOrgId: true,
parentOrgId: true
}); });
+25 -14
View File
@@ -150,36 +150,47 @@ export const orgServiceFactory = ({
/* /*
* Get organization details by the organization id * Get organization details by the organization id
* */ * */
const findOrganizationById = async ( const findOrganizationById = async ({
userId: string, userId,
orgId: string, orgId,
actorAuthMethod: ActorAuthMethod, actorAuthMethod,
rootOrgId: string, rootOrgId,
actorOrgId: string actorOrgId
) => { }: {
userId: string;
orgId: string;
actorAuthMethod: ActorAuthMethod;
rootOrgId: string;
actorOrgId: string;
}) => {
await permissionService.getOrgPermission({ await permissionService.getOrgPermission({
actor: ActorType.USER, actor: ActorType.USER,
actorId: userId, actorId: userId,
orgId, orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId: rootOrgId, actorOrgId,
scope: OrganizationActionScope.Any scope: OrganizationActionScope.Any
}); });
const appCfg = getConfig(); const appCfg = getConfig();
const org = await orgDAL.findOrgById(orgId); const hasSubOrg = rootOrgId !== actorOrgId;
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const org = await orgDAL.findOrgById(rootOrgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${rootOrgId}' not found` });
const hasSubOrg = actorOrgId !== rootOrgId;
let subOrg; let subOrg;
if (hasSubOrg) { if (hasSubOrg) {
subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId }); subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId });
if (!subOrg) throw new NotFoundError({ message: `Sub-organization with ID '${actorOrgId}' not found` });
} }
if (!org.userTokenExpiration) { const data = hasSubOrg && subOrg ? subOrg : org;
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg }; if (!data.userTokenExpiration) {
return { ...data, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
} }
return { ...org, subOrganization: subOrg }; return data;
}; };
/* /*
* Get all organization a user part of * Get all organization a user part of
* */ * */
@@ -640,7 +640,8 @@ export const superAdminServiceFactory = ({
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit,
name: "Instance Admin Token", name: "Instance Admin Token",
authMethod: IdentityAuthMethod.TOKEN_AUTH authMethod: IdentityAuthMethod.TOKEN_AUTH,
subOrganizationId: organization.id
}, },
tx tx
); );
@@ -45,8 +45,8 @@ Infisical supports the use of Username & Password authentication to connect with
</Tab> </Tab>
</Tabs> </Tabs>
</Step> </Step>
</Steps>
</Steps>
## Create MongoDB Connection in Infisical ## Create MongoDB Connection in Infisical
@@ -121,7 +121,7 @@ Infisical supports the use of Username & Password authentication to connect with
"createdAt": "2025-04-23T19:46:34.831Z", "createdAt": "2025-04-23T19:46:34.831Z",
"updatedAt": "2025-04-23T19:46:34.831Z", "updatedAt": "2025-04-23T19:46:34.831Z",
"isPlatformManagedCredentials": false, "isPlatformManagedCredentials": false,
"credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e",
"app": "mongodb", "app": "mongodb",
"method": "username-and-password", "method": "username-and-password",
"credentials": { "credentials": {
@@ -137,5 +137,5 @@ Infisical supports the use of Username & Password authentication to connect with
} }
``` ```
</Tab> </Tab>
</Tabs>
</Tabs>
-13
View File
@@ -24,8 +24,6 @@ apiRequest.interceptors.request.use((config) => {
const token = getAuthToken(); const token = getAuthToken();
const providerAuthToken = SecurityClient.getProviderAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken();
const params = new URLSearchParams(window.location.search);
if (config.headers) { if (config.headers) {
if (signupTempToken) { if (signupTempToken) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@@ -40,17 +38,6 @@ apiRequest.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`; config.headers.Authorization = `Bearer ${providerAuthToken}`;
} }
const rootOrgHeader = config.headers.get("x-root-org");
if (rootOrgHeader) {
config.headers.delete("x-root-org");
} else {
const subOrganization = params.get("subOrganization");
if (subOrganization) {
config.headers.set("x-infisical-org", subOrganization);
}
}
} }
return config; return config;
+2 -1
View File
@@ -61,7 +61,8 @@ export const leaveConfirmDefaultMessage =
export enum SessionStorageKeys { export enum SessionStorageKeys {
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN", CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL", ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL",
AUTH_CONSENT = "AUTH_CONSENT" AUTH_CONSENT = "AUTH_CONSENT",
MFA_TEMP_TOKEN = "MFA_TEMP_TOKEN"
} }
export const secretTagsColors = [ export const secretTagsColors = [
@@ -1,6 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { useRouteContext, useSearch } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
@@ -10,28 +10,23 @@ export const useOrganization = () => {
select: (el) => el.organizationId select: (el) => el.organizationId
}); });
const subOrganization = useSearch({
strict: false,
select: (el) => el?.subOrganization
});
const { data: currentOrg } = useSuspenseQuery({ const { data: currentOrg } = useSuspenseQuery({
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"), queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId), queryFn: () => fetchOrganizationById(organizationId),
staleTime: Infinity staleTime: Infinity
}); });
const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId);
const org = useMemo( const org = useMemo(
() => ({ () => ({
currentOrg: { currentOrg: {
...currentOrg, ...currentOrg,
id: currentOrg?.subOrganization?.id || currentOrg?.id, parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null
parentOrgId: currentOrg.id
}, },
isSubOrganization: Boolean(currentOrg.subOrganization), isSubOrganization,
isRootOrganization: !currentOrg.subOrganization isRootOrganization: !isSubOrganization
}), }),
[currentOrg, subOrganization] [currentOrg]
); );
return org; return org;
+5 -3
View File
@@ -58,10 +58,12 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
return data; return data;
}; };
export const selectOrganization = async (data: { export type SelectOrganizationParams = {
organizationId: string; organizationId: string;
userAgent?: UserAgentType; userAgent?: UserAgentType;
}) => { };
export const selectOrganization = async (data: SelectOrganizationParams) => {
const { data: res } = await apiRequest.post<{ const { data: res } = await apiRequest.post<{
token: string; token: string;
isMfaEnabled: boolean; isMfaEnabled: boolean;
@@ -73,7 +75,7 @@ export const selectOrganization = async (data: {
export const useSelectOrganization = () => { export const useSelectOrganization = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => { mutationFn: async (details: SelectOrganizationParams) => {
const data = await selectOrganization(details); const data = await selectOrganization(details);
// If a custom user agent is set, then this session is meant for another consuming application, not the web application. // If a custom user agent is set, then this session is meant for another consuming application, not the web application.
+1
View File
@@ -1,6 +1,7 @@
export type GetAuthTokenAPI = { export type GetAuthTokenAPI = {
token: string; token: string;
organizationId?: string; organizationId?: string;
subOrganizationId?: string;
}; };
export enum UserEncryptionVersion { export enum UserEncryptionVersion {
@@ -42,7 +42,7 @@ export const organizationKeys = {
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }], getOrgById: (orgId: string) => ["organization", { orgId }],
getAvailableIdentities: () => ["available-identities"], getAvailableIdentities: () => ["available-identities"],
getAvailableUsers: () => ["available-users"] getAvailableUsers: () => ["available-users"]
}; };
@@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => {
const { const {
data: { organization } data: { organization }
} = await apiRequest.get<{ } = await apiRequest.get<{
organization: Organization & { subOrganization?: { id: string; name: string } }; organization: Organization;
}>(`/api/v1/organization/${id}`); }>(`/api/v1/organization/${id}`);
return organization; return organization;
}; };
@@ -30,6 +30,8 @@ export type Organization = {
maxSharedSecretLifetime: number; maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null; maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean; blockDuplicateSecretSyncDestinations: boolean;
parentOrgId: string | null;
rootOrgId: string | null;
}; };
export type UpdateOrgDTO = { export type UpdateOrgDTO = {
@@ -3,14 +3,16 @@ import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "./types"; import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "./types";
export const projectKeys = { export const projectKeys = {
getProjectById: (projectId: string) => ["projects", { projectId }] as const, allProjectQueries: () => ["projects"] as const,
getProjectById: (projectId: string) =>
[...projectKeys.allProjectQueries(), { projectId }] as const,
getProjectSecrets: (projectId: string) => [{ projectId }, "project-secrets"] as const, getProjectSecrets: (projectId: string) => [{ projectId }, "project-secrets"] as const,
getProjectIndexStatus: (projectId: string) => [{ projectId }, "project-index-status"] as const, getProjectIndexStatus: (projectId: string) => [{ projectId }, "project-index-status"] as const,
getProjectUpgradeStatus: (projectId: string) => [{ projectId }, "project-upgrade-status"], getProjectUpgradeStatus: (projectId: string) => [{ projectId }, "project-upgrade-status"],
getProjectMemberships: (orgId: string) => [{ orgId }, "project-memberships"], getProjectMemberships: (orgId: string) => [{ orgId }, "project-memberships"],
getProjectAuthorization: (projectId: string) => [{ projectId }, "project-authorizations"], getProjectAuthorization: (projectId: string) => [{ projectId }, "project-authorizations"],
getProjectIntegrations: (projectId: string) => [{ projectId }, "project-integrations"], getProjectIntegrations: (projectId: string) => [{ projectId }, "project-integrations"],
getAllUserProjects: () => ["projects"] as const, getAllUserProjects: () => [...projectKeys.allProjectQueries()] as const,
getProjectAuditLogs: (projectId: string) => [{ projectId }, "project-audit-logs"] as const, getProjectAuditLogs: (projectId: string) => [{ projectId }, "project-audit-logs"] as const,
getProjectUsers: ( getProjectUsers: (
projectId: string, projectId: string,
@@ -28,7 +30,8 @@ export const projectKeys = {
// allows invalidation using above key without knowing params // allows invalidation using above key without knowing params
getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) => getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) =>
[...projectKeys.getProjectIdentityMemberships(projectId), params] as const, [...projectKeys.getProjectIdentityMemberships(projectId), params] as const,
searchProject: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, searchProject: (dto: TSearchProjectsDTO) =>
[...projectKeys.allProjectQueries(), "search-projects", dto] as const,
getProjectGroupMemberships: (projectId: string) => [{ projectId }, "project-groups"] as const, getProjectGroupMemberships: (projectId: string) => [{ projectId }, "project-groups"] as const,
getProjectGroupMembershipDetails: (projectId: string, groupId: string) => getProjectGroupMembershipDetails: (projectId: string, groupId: string) =>
[{ projectId, groupId }, "project-group-membership-details"] as const, [{ projectId, groupId }, "project-group-membership-details"] as const,
@@ -11,10 +11,7 @@ export const useCreateSubOrganization = () => {
mutationFn: async (dto: TCreateSubOrganizationDTO) => { mutationFn: async (dto: TCreateSubOrganizationDTO) => {
const { data } = await apiRequest.post<{ organization: TSubOrganization }>( const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
"/api/v1/sub-organizations", "/api/v1/sub-organizations",
dto, dto
{
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
}
); );
return data; return data;
}, },
@@ -80,7 +80,11 @@ const getPlan = (subscription: SubscriptionPlan) => {
return "Free"; return "Free";
}; };
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => { const getFormattedSupportEmailLink = (variables: {
org_id: string;
domain: string;
root_org_id?: string;
}) => {
const email = "[email protected]"; const email = "[email protected]";
const body = `Hello Infisical Support Team, const body = `Hello Infisical Support Team,
@@ -94,6 +98,7 @@ Issue Details:
Account Info: Account Info:
- Organization ID: ${variables.org_id} - Organization ID: ${variables.org_id}
${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""}
- Domain: ${variables.domain} - Domain: ${variables.domain}
Thank you, Thank you,
@@ -169,6 +174,10 @@ export const Navbar = () => {
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days); const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
const rootOrg = isSubOrganization
? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg
: currentOrg;
useEffect(() => { useEffect(() => {
if (isModalIntrusive) { if (isModalIntrusive) {
setShowCardDeclinedModal(true); setShowCardDeclinedModal(true);
@@ -182,10 +191,20 @@ export const Navbar = () => {
} }
}, [subscription, isBillingPage, isModalIntrusive]); }, [subscription, isBillingPage, isModalIntrusive]);
const handleOrgChange = async (orgId: string) => { const handleOrgSelection = async ({
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId,
organizationId: orgId navigateTo,
}); onSuccess
}: {
organizationId?: string;
navigateTo?: string;
onSuccess?: () => void | Promise<void>;
}) => {
if (!organizationId) return;
if (organizationId === currentOrg.id) return;
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
if (isMfaEnabled) { if (isMfaEnabled) {
SecurityClient.setMfaToken(token); SecurityClient.setMfaToken(token);
@@ -193,14 +212,58 @@ export const Navbar = () => {
setRequiredMfaMethod(mfaMethod); setRequiredMfaMethod(mfaMethod);
} }
toggleShowMfa.on(); toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleOrgChange(orgId)); setMfaSuccessCallback(() => async () => {
await handleOrgSelection({ organizationId, onSuccess });
});
return; return;
} }
await router.invalidate();
await navigateUserToOrg(navigate, orgId); SecurityClient.setToken(token);
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); SecurityClient.setProviderAuthToken("");
queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
await queryClient.refetchQueries({ queryKey: authKeys.getAuthToken });
await navigateUserToOrg({ navigate, organizationId, navigateTo });
queryClient.removeQueries({ queryKey: projectKeys.allProjectQueries() });
if (onSuccess) {
await onSuccess();
}
};
const handleNavigateToRootOrgBilling = async () => {
const navigateToBilling = () => {
navigate({
to: "/organizations/$orgId/billing",
params: { orgId: rootOrg.id }
});
};
const onSuccess = () => {
setShowCardDeclinedModal(false);
};
if (isSubOrganization) {
await handleOrgSelection({ organizationId: rootOrg.id, onSuccess });
} else {
await navigateToBilling();
}
};
const handleNavigateToAdminConsole = async () => {
const navigateToAdminConsole = () => {
navigate({
to: "/admin"
});
};
if (isSubOrganization) {
await handleOrgSelection({ organizationId: rootOrg.id, navigateTo: "/admin" });
} else {
navigateToAdminConsole();
}
}; };
const { mutateAsync } = useGetOrgTrialUrl(); const { mutateAsync } = useGetOrgTrialUrl();
@@ -272,7 +335,7 @@ export const Navbar = () => {
return; return;
} }
handleOrgChange(org?.id); handleOrgSelection({ organizationId: org?.id });
}; };
return ( return (
@@ -314,17 +377,20 @@ export const Navbar = () => {
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap" className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
type="button" type="button"
onClick={async () => { onClick={async () => {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
if (isSubOrganization) { if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null); await handleOrgSelection({
organizationId: currentOrg.rootOrgId as string
});
} else {
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: currentOrg.id }
});
} }
}} }}
> >
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} /> <OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
<span className="truncate">{currentOrg?.name}</span> <span className="truncate">{rootOrg?.name}</span>
<Badge variant="org" className="hidden lg:inline-flex"> <Badge variant="org" className="hidden lg:inline-flex">
Organization Organization
</Badge> </Badge>
@@ -397,13 +463,7 @@ export const Navbar = () => {
</div> </div>
{subOrganizations.map((subOrg) => ( {subOrganizations.map((subOrg) => (
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
navigate({
to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}}
className="cursor-pointer font-normal" className="cursor-pointer font-normal"
key={subOrg.id} key={subOrg.id}
> >
@@ -458,79 +518,93 @@ export const Navbar = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{currentOrg.subOrganization && ( {isSubOrganization && (
<> <>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <ChevronRight size={18} className="mt-[3px] mr-3 text-mineshaft-400/70" />
<DropdownMenu modal={false}> <div
<Badge className={twMerge(
asChild "relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
isTruncatable !isProjectScope && isSubOrganization
variant="sub-org" ? "border-sub-org/15 bg-gradient-to-b from-sub-org/10 to-sub-org/[0.075]"
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar : "border-transparent"
className={twMerge( )}
"gap-x-1.5 text-sm", >
isProjectScope && {/* scott: the below is used to hide the top border from the org nav bar */}
"min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org" {!isProjectScope && isSubOrganization && (
)} <div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
> <div className="h-full bg-sub-org/[0.075]" />
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
<SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span>
</Link>
</Badge>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div> </div>
</DropdownMenuTrigger> )}
<DropdownMenuContent <DropdownMenu modal={false}>
align="center" <div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
side="bottom" <button
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md" className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
style={{ minWidth: "220px" }} type="button"
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
Sub-Organizations
</div>
{subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={async () => { onClick={async () => {
navigate({ navigate({
to: "/organizations/$orgId/projects", to: "/organizations/$orgId/projects",
params: { orgId: subOrg.id } params: { orgId: currentOrg.id }
}); });
await router.invalidate({ sync: true }).catch(() => null); if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}} }}
className="cursor-pointer font-normal"
key={subOrg.id}
> >
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2"> <SubOrgIcon className={twMerge("size-[14px] shrink-0 text-sub-org")} />
{currentOrg?.id === subOrg.id && ( <span className="truncate">{currentOrg?.name}</span>
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" /> <Badge variant="sub-org" className="hidden lg:inline-flex">
)} Sub-Organization
<p className="truncate">{subOrg.name}</p> </Badge>
</div> </button>
</DropdownMenuItem> </div>
))} <DropdownMenuTrigger asChild>
{Boolean(subOrganizations.length) && ( <div>
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <IconButton
)} variant="plain"
<DropdownMenuItem colorSchema="secondary"
className="cursor-pointer" ariaLabel="switch-org"
icon={<FontAwesomeIcon icon={faPlus} />} className="px-2 py-1"
onClick={() => setShowSubOrgForm(true)} >
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
> >
New Sub-Organization <div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
</DropdownMenuItem> Sub-Organizations
</DropdownMenuContent> </div>
</DropdownMenu> {subOrganizations.map((subOrg) => (
<DropdownMenuItem
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
className="cursor-pointer font-normal"
key={subOrg.id}
>
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
{currentOrg?.id === subOrg.id && (
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
)}
<p className="truncate">{subOrg.name}</p>
</div>
</DropdownMenuItem>
))}
{Boolean(subOrganizations.length) && (
<div className="mt-1 h-1 border-t border-mineshaft-600" />
)}
<DropdownMenuItem
className="cursor-pointer"
icon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => setShowSubOrgForm(true)}
>
New Sub-Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</> </>
)} )}
{isProjectScope && ( {isProjectScope && (
@@ -550,11 +624,11 @@ export const Navbar = () => {
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600" className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faInfinity} />} leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => { onClick={async () => {
if (!subscription || !currentOrg) return; if (!subscription || !rootOrg) return;
// direct user to start pro trial // direct user to start pro trial
const url = await mutateAsync({ const url = await mutateAsync({
orgId: currentOrg.id, orgId: rootOrg.id,
success_url: window.location.href success_url: window.location.href
}); });
@@ -575,6 +649,7 @@ export const Navbar = () => {
<Link <Link
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600" className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin" to="/admin"
onClick={handleNavigateToAdminConsole}
> >
<InstanceIcon className="inline-block size-3.5" /> <InstanceIcon className="inline-block size-3.5" />
<span className="ml-2 hidden md:inline-block">Server Console</span> <span className="ml-2 hidden md:inline-block">Server Console</span>
@@ -612,7 +687,8 @@ export const Navbar = () => {
text === "Email Support" text === "Email Support"
? getUrl({ ? getUrl({
org_id: currentOrg.id, org_id: currentOrg.id,
domain: window.location.origin domain: window.location.origin,
...(isSubOrganization && { root_org_id: rootOrg.id })
}) })
: getUrl(); : getUrl();
@@ -772,19 +848,13 @@ export const Navbar = () => {
</div> </div>
<div className="mt-4"> <div className="mt-4">
<div className="flex space-x-3"> <div className="flex space-x-3">
<Link <Button
to="/organizations/$orgId/billing" colorSchema="primary"
params={{ orgId: currentOrg.id }} variant="solid"
className="inline-flex" onClick={handleNavigateToRootOrgBilling}
> >
<Button Update Payment Method
colorSchema="primary" </Button>
variant="solid"
onClick={() => setShowCardDeclinedModal(false)}
>
Update Payment Method
</Button>
</Link>
{!isModalIntrusive && ( {!isModalIntrusive && (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
@@ -810,6 +880,7 @@ export const Navbar = () => {
onClose={() => { onClose={() => {
setShowSubOrgForm(false); setShowSubOrgForm(false);
}} }}
handleOrgSelection={handleOrgSelection}
/> />
</div> </div>
</ModalContent> </ModalContent>
@@ -1,15 +1,18 @@
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useCreateSubOrganization } from "@app/hooks/api"; import { useCreateSubOrganization } from "@app/hooks/api";
import { selectOrganization } from "@app/hooks/api/auth/queries";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
type ContentProps = { type ContentProps = {
onClose: () => void; onClose: () => void;
handleOrgSelection: (params: { organizationId: string }) => void;
}; };
const AddOrgSchema = z.object({ const AddOrgSchema = z.object({
@@ -18,7 +21,8 @@ const AddOrgSchema = z.object({
type FormData = z.infer<typeof AddOrgSchema>; type FormData = z.infer<typeof AddOrgSchema>;
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { export const NewSubOrganizationForm = ({ onClose, handleOrgSelection }: ContentProps) => {
const { currentOrg, isSubOrganization } = useOrganization();
const createSubOrg = useCreateSubOrganization(); const createSubOrg = useCreateSubOrganization();
const { const {
@@ -32,10 +36,16 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
resolver: zodResolver(AddOrgSchema) resolver: zodResolver(AddOrgSchema)
}); });
const navigate = useNavigate();
const router = useRouter();
const onSubmit = async ({ name }: FormData) => { const onSubmit = async ({ name }: FormData) => {
if (isSubOrganization && currentOrg.rootOrgId) {
const { token } = await selectOrganization({
organizationId: currentOrg.rootOrgId
});
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
}
const { organization } = await createSubOrg.mutateAsync({ const { organization } = await createSubOrg.mutateAsync({
name name
}); });
@@ -46,11 +56,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
}); });
onClose(); onClose();
navigate({ await handleOrgSelection({ organizationId: organization.id });
to: "/organizations/$orgId/projects",
params: { orgId: organization.id }
});
await router.invalidate({ sync: true }).catch(() => null);
}; };
return ( return (
@@ -1,5 +1,6 @@
import { Link, useLocation } from "@tanstack/react-router"; import { Link, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Tab, TabList, Tabs } from "@app/components/v2"; import { Tab, TabList, Tabs } from "@app/components/v2";
@@ -21,7 +22,14 @@ export const OrgNavBar = ({ isHidden }: Props) => {
return ( return (
<div className="bg-mineshaft-900"> <div className="bg-mineshaft-900">
{!isHidden && ( {!isHidden && (
<div className="dark flex w-full flex-col overflow-x-hidden border-y border-t-org/15 border-b-org/5 bg-gradient-to-b from-org/[0.075] to-org/[0.025] px-4 pt-0.5"> <div
className={twMerge(
"dark flex w-full flex-col overflow-x-hidden border-y bg-gradient-to-b px-4 pt-0.5",
isRootOrganization
? "border-t-org/15 border-b-org/5 from-org/[0.075] to-org/[0.025]"
: "border-t-sub-org/15 border-b-sub-org/5 from-sub-org/[0.075] to-sub-org/[0.025]"
)}
>
<motion.div <motion.div
key="menu-org-items" key="menu-org-items"
initial={{ x: -150 }} initial={{ x: -150 }}
@@ -8,7 +8,7 @@ import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
export const AssumePrivilegeModeBanner = () => { export const AssumePrivilegeModeBanner = () => {
const { isSubOrganization, currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentProject } = useProject(); const { currentProject } = useProject();
const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege(); const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege();
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
@@ -37,7 +37,7 @@ export const AssumePrivilegeModeBanner = () => {
}, },
{ {
onSuccess: () => { onSuccess: () => {
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`; const url = getProjectHomePage(currentProject.type, currentProject.environments);
window.location.assign( window.location.assign(
url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id) url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id)
); );
@@ -157,20 +157,11 @@ const ProjectSelectInner = () => {
params: { params: {
projectId: workspace.id, projectId: workspace.id,
orgId: workspace.orgId orgId: workspace.orgId
},
search: {
subOrganization: currentOrg?.subOrganization?.name
} }
}); });
const urlInstance = new URL( const urlInstance = new URL(
`${window.location.origin}${url.to.replaceAll("$orgId", url.params.orgId).replaceAll("$projectId", url.params.projectId)}` `${window.location.origin}${url.to.replaceAll("$orgId", url.params.orgId).replaceAll("$projectId", url.params.projectId)}`
); );
if (currentOrg?.subOrganization) {
urlInstance.searchParams.set(
"subOrganization",
currentOrg.subOrganization.name
);
}
window.location.assign(urlInstance); window.location.assign(urlInstance);
}} }}
icon={ icon={
@@ -5,7 +5,17 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { queryClient } from "@app/hooks/api/reactQuery"; import { queryClient } from "@app/hooks/api/reactQuery";
import { userKeys } from "@app/hooks/api/users"; import { userKeys } from "@app/hooks/api/users";
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => { type NavigateUserToOrgParams = {
navigate: NavigateFn;
organizationId?: string;
navigateTo?: string;
};
export const navigateUserToOrg = async ({
navigate,
organizationId,
navigateTo
}: NavigateUserToOrgParams) => {
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations();
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced); const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
@@ -13,7 +23,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
if (organizationId) { if (organizationId) {
localStorage.setItem("orgData.id", organizationId); localStorage.setItem("orgData.id", organizationId);
navigate({ navigate({
to: "/organizations/$orgId/projects", to: navigateTo || "/organizations/$orgId/projects",
params: { orgId: organizationId } params: { orgId: organizationId }
}); });
return; return;
@@ -24,7 +34,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id; const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
localStorage.setItem("orgData.id", userOrg); localStorage.setItem("orgData.id", userOrg);
navigate({ navigate({
to: "/organizations/$orgId/projects", to: navigateTo || "/organizations/$orgId/projects",
params: { orgId: userOrg } params: { orgId: userOrg }
}); });
} else { } else {
@@ -115,7 +115,7 @@ export const PasswordStep = ({
return; return;
} }
await navigateUserToOrg(navigate, organizationId); await navigateUserToOrg({ navigate, organizationId });
}; };
await finishWithOrgWorkflow(); await finishWithOrgWorkflow();
@@ -131,7 +131,7 @@ export const PasswordStep = ({
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
await navigateUserToOrg(navigate); await navigateUserToOrg({ navigate });
} }
} }
} catch (err: any) { } catch (err: any) {
@@ -233,7 +233,7 @@ export const PasswordStep = ({
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
await navigateUserToOrg(navigate); await navigateUserToOrg({ navigate });
} }
} }
} else { } else {
@@ -254,7 +254,7 @@ export const PasswordStep = ({
// case: organization ID is present from the provider auth token -- navigate directly to the org // case: organization ID is present from the provider auth token -- navigate directly to the org
if (organizationId) { if (organizationId) {
await navigateUserToOrg(navigate, organizationId); await navigateUserToOrg({ navigate, organizationId });
} }
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs // case: no organization ID is present -- navigate to the select org page IF the user has any orgs
// if the user has no orgs, navigate to the create org page // if the user has no orgs, navigate to the create org page
@@ -264,7 +264,7 @@ export const PasswordStep = ({
if (userOrgs.length > 0) { if (userOrgs.length > 0) {
navigateToSelectOrganization(undefined, isAdminLogin); navigateToSelectOrganization(undefined, isAdminLogin);
} else { } else {
await navigateUserToOrg(navigate); await navigateUserToOrg({ navigate });
} }
} }
} }
@@ -316,7 +316,7 @@ export const PasswordStep = ({
return ( return (
<EmailDuplicationConfirmation <EmailDuplicationConfirmation
onRemoveDuplicateLater={() => onRemoveDuplicateLater={() =>
navigateUserToOrg(navigate, organizationId).catch(() => navigateUserToOrg({ navigate, organizationId }).catch(() =>
createNotification({ text: "Failed to navigate user", type: "error" }) createNotification({ text: "Failed to navigate user", type: "error" })
) )
} }
@@ -47,6 +47,7 @@ export const SelectOrganizationSection = () => {
const orgId = queryParams.get("org_id"); const orgId = queryParams.get("org_id");
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
const isAdminLogin = queryParams.get("is_admin_login") === "true"; const isAdminLogin = queryParams.get("is_admin_login") === "true";
const mfaPending = queryParams.get("mfa_pending") === "true";
const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId); const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId);
const logout = useLogoutUser(true); const logout = useLogoutUser(true);
@@ -188,7 +189,7 @@ export const SelectOrganizationSection = () => {
navigate({ to: "/cli-redirect" }); navigate({ to: "/cli-redirect" });
// cli page // cli page
} else { } else {
navigateUserToOrg(navigate, organization.id); navigateUserToOrg({ navigate, organizationId: organization.id });
} }
}, },
[selectOrg] [selectOrg]
@@ -201,7 +202,7 @@ export const SelectOrganizationSection = () => {
const decodedJwt = jwtDecode(authToken) as any; const decodedJwt = jwtDecode(authToken) as any;
if (decodedJwt?.organizationId) { if (decodedJwt?.organizationId) {
navigateUserToOrg(navigate, decodedJwt.organizationId); navigateUserToOrg({ navigate, organizationId: decodedJwt.organizationId });
} }
} }
@@ -236,10 +237,22 @@ export const SelectOrganizationSection = () => {
}, [organizations.isPending, organizations.data]); }, [organizations.isPending, organizations.data]);
useEffect(() => { useEffect(() => {
if (mfaPending && defaultSelectedOrg) {
const storedMfaToken = sessionStorage.getItem(SessionStorageKeys.MFA_TEMP_TOKEN);
if (storedMfaToken) {
sessionStorage.removeItem(SessionStorageKeys.MFA_TEMP_TOKEN);
SecurityClient.setMfaToken(storedMfaToken);
setIsInitialOrgCheckLoading(false);
toggleShowMfa.on();
setMfaSuccessCallback(() => () => handleSelectOrganization(defaultSelectedOrg));
return;
}
}
if (defaultSelectedOrg) { if (defaultSelectedOrg) {
handleSelectOrganization(defaultSelectedOrg); handleSelectOrganization(defaultSelectedOrg);
} }
}, [defaultSelectedOrg]); }, [defaultSelectedOrg, mfaPending]);
if ( if (
userLoading || userLoading ||
@@ -8,7 +8,8 @@ export const SelectOrganizationPageQueryParams = z.object({
org_id: z.string().optional().catch(""), org_id: z.string().optional().catch(""),
callback_port: z.coerce.number().optional().catch(undefined), callback_port: z.coerce.number().optional().catch(undefined),
is_admin_login: z.boolean().optional().catch(false), is_admin_login: z.boolean().optional().catch(false),
force: z.boolean().optional() force: z.boolean().optional(),
mfa_pending: z.boolean().optional().catch(false)
}); });
export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({ export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({
@@ -16,7 +17,12 @@ export const Route = createFileRoute("/_restrict-login-signup/login/select-organ
validateSearch: zodValidator(SelectOrganizationPageQueryParams), validateSearch: zodValidator(SelectOrganizationPageQueryParams),
search: { search: {
middlewares: [ middlewares: [
stripSearchParams({ org_id: "", callback_port: undefined, is_admin_login: false }) stripSearchParams({
org_id: "",
callback_port: undefined,
is_admin_login: false,
mfa_pending: false
})
] ]
} }
}); });
@@ -18,7 +18,7 @@ const tabs = [
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
@@ -19,7 +19,7 @@ const tabs = [
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -39,7 +39,8 @@ export const SettingsPage = () => {
}} }}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
> >
<InfoIcon size={12} /> Looking for organization settings? <InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
settings?
</Link> </Link>
</PageHeader> </PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
@@ -73,6 +73,11 @@ export const Route = createFileRoute("/_authenticate")({
}); });
}); });
return { organizationId: data.organizationId as string, isAuthenticated: true, user }; const isSubOrganization = !!data.subOrganizationId;
return {
organizationId: isSubOrganization ? data.subOrganizationId : (data.organizationId as string),
isAuthenticated: true,
user
};
} }
}); });
@@ -1,7 +1,12 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, isRedirect, redirect } from "@tanstack/react-router";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { SessionStorageKeys } from "@app/const";
import { authKeys, fetchAuthToken, selectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
import { projectKeys } from "@app/hooks/api/projects";
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { subOrganizationsQuery } from "@app/hooks/api/subOrganizations";
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
// Route context to fill in organization's data like details, subscription etc // Route context to fill in organization's data like details, subscription etc
@@ -15,6 +20,44 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details")({
organizationId = context.organizationId!; organizationId = context.organizationId!;
} }
if ((params as { orgId?: string })?.orgId && context.organizationId) {
const urlOrgId = (params as { orgId: string }).orgId;
const currentTokenOrgId = context.organizationId;
if (urlOrgId !== currentTokenOrgId) {
try {
const { token, isMfaEnabled } = await selectOrganization({ organizationId: urlOrgId });
if (isMfaEnabled) {
sessionStorage.setItem(SessionStorageKeys.MFA_TEMP_TOKEN, token);
throw redirect({
to: "/login/select-organization",
search: { org_id: urlOrgId, mfa_pending: true }
});
}
if (!isMfaEnabled && token) {
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
context.queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
context.queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
context.queryClient.removeQueries({ queryKey: subOrganizationsQuery.allKey() });
await context.queryClient.fetchQuery({
queryKey: authKeys.getAuthToken,
queryFn: fetchAuthToken
});
}
} catch (error) {
if (isRedirect(error)) {
throw error;
}
console.warn("Failed to automatically exchange token for organization:", error);
}
}
}
await context.queryClient.ensureQueryData({ await context.queryClient.ensureQueryData({
queryKey: organizationKeys.getOrgById(organizationId), queryKey: organizationKeys.getOrgById(organizationId),
queryFn: () => fetchOrganizationById(organizationId) queryFn: () => fetchOrganizationById(organizationId)
@@ -117,9 +117,10 @@ export const Route = createFileRoute("/_restrict-login-signup")({
return; return;
throw redirect({ to: "/login/select-organization" }); throw redirect({ to: "/login/select-organization" });
} }
const orgId = data.subOrganizationId || data.organizationId;
throw redirect({ throw redirect({
to: "/organizations/$orgId/projects", to: "/organizations/$orgId/projects",
params: { orgId: data.organizationId } params: { orgId }
}); });
}, },
component: AuthConsentWrapper component: AuthConsentWrapper
@@ -3,7 +3,8 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan"; import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
@@ -84,8 +85,20 @@ export const AccessManagementPage = () => {
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`} title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources." description={`Manage fine-grained access for users, groups, roles, and machine identities within your ${isSubOrganization ? "sub-" : ""}organization resources.`}
/> >
{isSubOrganization && (
<Link
to="/organizations/$orgId/access-management"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization access control?
</Link>
)}
</PageHeader>
{!currentOrg.shouldUseNewPrivilegeSystem && ( {!currentOrg.shouldUseNewPrivilegeSystem && (
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5"> <div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm"> <div className="mb-1 flex items-center text-sm">
@@ -6,7 +6,12 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3"; import { DocumentationLinkBadge } from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import {
OrgPermissionGroupActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { useDeleteGroup } from "@app/hooks/api"; import { useDeleteGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -15,6 +20,7 @@ import { OrgGroupsTable } from "./OrgGroupsTable";
export const OrgGroupsSection = () => { export const OrgGroupsSection = () => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { isSubOrganization } = useOrganization();
const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -51,7 +57,9 @@ export const OrgGroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Organization Groups</p> <p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Groups
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
</div> </div>
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
@@ -63,7 +71,7 @@ export const OrgGroupsSection = () => {
onClick={() => handleAddGroupModal()} onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Create Organization Group Create {isSubOrganization ? "Sub-" : ""}Organization Group
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -71,7 +71,7 @@ enum GroupsOrderBy {
export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { isPending, data: groups = [] } = useGetOrganizationGroups(orgId); const { isPending, data: groups = [] } = useGetOrganizationGroups(orgId);
const { mutateAsync: updateMutateAsync } = useUpdateGroup(); const { mutateAsync: updateMutateAsync } = useUpdateGroup();
@@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organization groups..." placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization groups...`}
/> />
<TableContainer className="mt-4"> <TableContainer className="mt-4">
<Table> <Table>
@@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
</Th> </Th>
<Th> <Th>
<div className="flex items-center"> <div className="flex items-center">
Organization Role {isSubOrganization ? "Sub-" : ""}Organization Role
<IconButton <IconButton
variant="plain" variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`} className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}
@@ -389,8 +389,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<EmptyState <EmptyState
title={ title={
groups.length groups.length
? "No organization groups match search..." ? `No ${isSubOrganization ? "sub-" : ""}organization groups match search...`
: "No organization groups found" : `No ${isSubOrganization ? "sub-" : ""}organization groups found`
} }
icon={groups.length ? faSearch : faUsers} icon={groups.length ? faSearch : faUsers}
/> />
@@ -1,13 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion"; import { InfoIcon } from "lucide-react";
import { LinkIcon, PlusIcon } from "lucide-react"; import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; import { Button, DeleteActionModal, Modal, ModalContent, Tooltip } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3"; import { DocumentationLinkBadge } from "@app/components/v3";
import { import {
OrgPermissionIdentityActions, OrgPermissionIdentityActions,
@@ -30,9 +30,8 @@ import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm";
import { OrgIdentityModal } from "./OrgIdentityModal"; import { OrgIdentityModal } from "./OrgIdentityModal";
enum IdentityWizardSteps { enum IdentityWizardSteps {
SelectAction = "select-action", CreateIdentity = "create-identity",
LinkIdentity = "link-identity", LinkIdentity = "link-identity"
OrganizationIdentity = "project-identity"
} }
export const IdentitySection = withPermission( export const IdentitySection = withPermission(
@@ -41,7 +40,7 @@ export const IdentitySection = withPermission(
const { currentOrg, isSubOrganization } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.SelectAction); const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.CreateIdentity);
const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity(); const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity();
const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate(); const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate();
@@ -100,7 +99,7 @@ export const IdentitySection = withPermission(
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-1 items-center gap-x-2"> <div className="flex flex-1 items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100"> <p className="text-xl font-medium text-mineshaft-100">
Organization Machine Identities {isSubOrganization ? "Sub-" : ""}Organization Machine Identities
</p> </p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
</div> </div>
@@ -124,7 +123,7 @@ export const IdentitySection = withPermission(
} }
if (!isSubOrganization) { if (!isSubOrganization) {
setWizardStep(IdentityWizardSteps.OrganizationIdentity); setWizardStep(IdentityWizardSteps.CreateIdentity);
} }
handlePopUpOpen("identity"); handlePopUpOpen("identity");
@@ -197,7 +196,7 @@ export const IdentitySection = withPermission(
onOpenChange={(open) => { onOpenChange={(open) => {
handlePopUpToggle("identity", open); handlePopUpToggle("identity", open);
if (!open) { if (!open) {
setWizardStep(IdentityWizardSteps.SelectAction); setWizardStep(IdentityWizardSteps.CreateIdentity);
} }
}} }}
> >
@@ -214,80 +213,84 @@ export const IdentitySection = withPermission(
: undefined : undefined
} }
> >
<AnimatePresence mode="wait"> {isSubOrganization && (
{wizardStep === IdentityWizardSteps.SelectAction && ( <div className="mb-4 flex items-center justify-center gap-x-2">
<motion.div <div className="flex w-3/4 gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
key="select-type-step" <Button
transition={{ duration: 0.1 }} variant="outline_bg"
initial={{ opacity: 0, translateX: 30 }} onClick={() => {
animate={{ opacity: 1, translateX: 0 }} setWizardStep(IdentityWizardSteps.CreateIdentity);
exit={{ opacity: 0, translateX: -30 }}
>
<div
className="cursor-pointer rounded-md border border-mineshaft-600 p-4 transition-all hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setWizardStep(IdentityWizardSteps.OrganizationIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(IdentityWizardSteps.OrganizationIdentity);
}
}} }}
size="xs"
className={twMerge(
"min-w-[2.4rem] flex-1 rounded border-none hover:bg-mineshaft-600",
wizardStep === IdentityWizardSteps.CreateIdentity
? "bg-mineshaft-500"
: "bg-transparent"
)}
> >
<div className="flex items-center gap-2"> Create New
<PlusIcon size="1rem" /> </Button>
<div>Create Machine Identity</div> <Button
</div> variant="outline_bg"
<div className="mt-2 text-xs text-mineshaft-300"> onClick={() => {
Create a new machine identity specifically for this sub-organization. This setWizardStep(IdentityWizardSteps.LinkIdentity);
machine identity will be managed at the sub-organization level.
</div>
</div>
<div
className="mt-4 cursor-pointer rounded-md border border-mineshaft-600 p-4 transition-all hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setWizardStep(IdentityWizardSteps.LinkIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(IdentityWizardSteps.LinkIdentity);
}
}} }}
size="xs"
className={twMerge(
"min-w-[2.4rem] flex-1 rounded border-none hover:bg-mineshaft-600",
wizardStep === IdentityWizardSteps.LinkIdentity
? "bg-mineshaft-500"
: "bg-transparent"
)}
> >
<div className="flex items-center gap-2"> Assign Existing
<LinkIcon size="1rem" /> </Button>
<div>Assign Existing Machine Identity</div> </div>
</div> <Tooltip
<div className="mt-2 text-xs text-mineshaft-300"> className="max-w-sm"
Assign an existing machine identity from your parent organization. The machine position="right"
identity will continue to be managed at its original scope. align="start"
</div> content={
</div> <>
</motion.div> <p className="mb-2 text-mineshaft-300">
)} You can add machine identities to your sub-organization in one of two ways:
{wizardStep === IdentityWizardSteps.OrganizationIdentity && ( </p>
<motion.div <ul className="ml-3.5 flex list-disc flex-col gap-y-4">
key="identity-step" <li className="text-mineshaft-200">
transition={{ duration: 0.1 }} <strong className="font-medium text-mineshaft-100">Create New</strong> -
initial={{ opacity: 0, translateX: 30 }} Create a new machine identity specifically for this sub-organization. This
animate={{ opacity: 1, translateX: 0 }} machine identity will be managed at the sub-organization level.
exit={{ opacity: 0, translateX: -30 }} <p className="mt-2">
This method is recommended for autonomous teams that need to manage
machine identity authentication.
</p>
</li>
<li>
<strong className="font-medium text-mineshaft-100">
Assign Existing
</strong>{" "}
Assign an existing machine identity from your parent organization. The
machine identity will continue to be managed at its original scope.
<p className="mt-2">
This method is recommended for organizations that need to maintain
centralized control.
</p>
</li>
</ul>
</>
}
> >
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <InfoIcon size={16} className="text-mineshaft-400" />
</motion.div> </Tooltip>
)} </div>
{wizardStep === IdentityWizardSteps.LinkIdentity && ( )}
<motion.div {wizardStep === IdentityWizardSteps.CreateIdentity && (
key="link-step" <OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
transition={{ duration: 0.1 }} )}
initial={{ opacity: 0, translateX: 30 }} {wizardStep === IdentityWizardSteps.LinkIdentity && (
animate={{ opacity: 1, translateX: 0 }} <OrgIdentityLinkForm onClose={() => handlePopUpClose("identity")} />
exit={{ opacity: 0, translateX: -30 }} )}
>
<OrgIdentityLinkForm onClose={() => handlePopUpClose("identity")} />
</motion.div>
)}
</AnimatePresence>
</ModalContent> </ModalContent>
</Modal> </Modal>
<DeleteActionModal <DeleteActionModal
@@ -200,7 +200,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none"> <DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900"> <DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Filter Organization Machine Identities by Role Filter {isSubOrganization ? "Sub-" : ""}Organization Machine Identities by Role
</DropdownMenuLabel> </DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => ( {roles?.map(({ id, slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
@@ -229,7 +229,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search machine identities by name..." placeholder={`Search ${isSubOrganization ? "sub-organization" : "organization"} machine identities by name...`}
/> />
</div> </div>
<TableContainer> <TableContainer>
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</Th> </Th>
<Th> <Th>
<div className="flex items-center"> <div className="flex items-center">
Organization Role {isSubOrganization ? "Sub-" : ""}Organization Role
<IconButton <IconButton
variant="plain" variant="plain"
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`} className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
@@ -304,7 +304,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
to: "/organizations/$orgId/identities/$identityId", to: "/organizations/$orgId/identities/$identityId",
params: { params: {
identityId: id, identityId: id,
orgId orgId: currentOrg.id
} }
}) })
} }
@@ -455,8 +455,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<EmptyState <EmptyState
title={ title={
debouncedSearch.trim().length > 0 || filter.roles?.length > 0 debouncedSearch.trim().length > 0 || filter.roles?.length > 0
? "No machine identities match search filter" ? `No ${isSubOrganization ? "sub-" : ""}organization machine identities match search filter`
: "No machine identities have been created in this organization" : `No machine identities have been created in this ${isSubOrganization ? "sub-" : ""}organization`
} }
icon={faServer} icon={faServer}
/> />
@@ -205,7 +205,9 @@ export const OrgMembersSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Organization Users</p> <p className="text-xl font-medium text-mineshaft-100">
{isSubOrganization ? "Sub-" : ""}Organization Users
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
</div> </div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
@@ -242,7 +244,7 @@ export const OrgMembersSection = () => {
isOpen={popUp.addMemberToSubOrg.isOpen} isOpen={popUp.addMemberToSubOrg.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addMemberToSubOrg", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("addMemberToSubOrg", isOpen)}
> >
<ModalContent title="Add member from your organization"> <ModalContent title="Add member from your organization" bodyClassName="overflow-visible">
<AddSubOrgMemberModal onClose={() => handlePopUpClose("addMemberToSubOrg")} /> <AddSubOrgMemberModal onClose={() => handlePopUpClose("addMemberToSubOrg")} />
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -336,7 +336,7 @@ export const OrgMembersTable = ({
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none"> <DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900"> <DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Filter Organization Users by Role Filter {isSubOrganization ? "Sub-" : ""}Organization Users by Role
</DropdownMenuLabel> </DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => ( {roles?.map(({ id, slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
@@ -365,7 +365,7 @@ export const OrgMembersTable = ({
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organization users..." placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization users...`}
/> />
</div> </div>
<TableContainer className="mt-4"> <TableContainer className="mt-4">
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
</Th> </Th>
<Th className="w-1/3"> <Th className="w-1/3">
<div className="flex items-center"> <div className="flex items-center">
Organization Role {isSubOrganization ? "Sub-" : ""}Organization Role
<IconButton <IconButton
variant="plain" variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`} className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
@@ -727,8 +727,8 @@ export const OrgMembersTable = ({
<EmptyState <EmptyState
title={ title={
members.length members.length
? "No organization users match search..." ? `No ${isSubOrganization ? "sub-" : ""}organization users match search...`
: "No organization users found" : `No ${isSubOrganization ? "sub-" : ""}organization users found`
} }
icon={members.length ? faSearch : faUsers} icon={members.length ? faSearch : faUsers}
/> />
@@ -207,7 +207,7 @@ export const OrgRoleTable = () => {
}} }}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Organization Role Add {isSubOrganization ? "Sub-" : ""}Organization Role
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -216,7 +216,7 @@ export const OrgRoleTable = () => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organization roles..." placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization roles...`}
className="flex-1" className="flex-1"
containerClassName="mb-4" containerClassName="mb-4"
/> />
@@ -1,4 +1,6 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
@@ -6,7 +8,7 @@ import { useOrganization } from "@app/context";
import { LogsSection } from "./components"; import { LogsSection } from "./components";
export const AuditLogsPage = () => { export const AuditLogsPage = () => {
const { isSubOrganization } = useOrganization(); const { isSubOrganization, currentOrg } = useOrganization();
return ( return (
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
@@ -21,7 +23,19 @@ export const AuditLogsPage = () => {
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`} title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> >
{isSubOrganization && (
<Link
to="/organizations/$orgId/audit-logs"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization audit logs?
</Link>
)}
</PageHeader>
<LogsSection pageView /> <LogsSection pageView />
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Organization Groups {isSubOrganization ? "Sub-" : ""}Organization Groups
</Link> </Link>
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
@@ -8,7 +10,7 @@ import { OrgTabGroup } from "./components";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isSubOrganization } = useOrganization(); const { isSubOrganization, currentOrg } = useOrganization();
return ( return (
<> <>
@@ -19,9 +21,21 @@ export const SettingsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings" description={`Configure ${isSubOrganization ? "sub-" : ""}organization-wide settings`}
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"} title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
/> >
{isSubOrganization && (
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.rootOrgId ?? ""
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for root organization settings?
</Link>
)}
</PageHeader>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
</div> </div>
@@ -53,7 +53,7 @@ const OrgConfigSection = ({
resolver: zodResolver(orgConfigFormSchema) resolver: zodResolver(orgConfigFormSchema)
}); });
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const { mutateAsync: setupOrgKmip } = useSetupOrgKmip(currentOrg.id); const { mutateAsync: setupOrgKmip } = useSetupOrgKmip(currentOrg.id);
const onFormSubmit = async (formData: TKmipOrgConfigForm) => { const onFormSubmit = async (formData: TKmipOrgConfigForm) => {
@@ -175,7 +175,10 @@ const OrgConfigSection = ({
)} )}
{!isKmipConfigLoading && !kmipConfig && ( {!isKmipConfigLoading && !kmipConfig && (
<div className="mt-2"> <div className="mt-2">
<div>KMIP has not yet been configured for the organization.</div> <div>
KMIP has not yet been configured for the {isSubOrganization ? "sub-" : ""}
organization.
</div>
<Button <Button
className="mt-2" className="mt-2"
onClick={() => { onClick={() => {

Some files were not shown because too many files have changed in this diff Show More