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