From e9ba9c0867655996cefe7b8fb607ed2a93bc917f Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Fri, 28 Nov 2025 18:00:59 +0530 Subject: [PATCH] fix: sub-org support in login endpoint for machine identities --- backend/src/lib/api-docs/constants.ts | 38 ++++++++++++------ .../v1/identity-alicloud-auth-router.ts | 6 ++- .../routes/v1/identity-aws-iam-auth-router.ts | 8 +++- .../routes/v1/identity-azure-auth-router.ts | 3 +- .../routes/v1/identity-gcp-auth-router.ts | 8 +++- .../routes/v1/identity-jwt-auth-router.ts | 6 ++- .../v1/identity-kubernetes-auth-router.ts | 6 ++- .../routes/v1/identity-ldap-auth-router.ts | 6 ++- .../routes/v1/identity-oci-auth-router.ts | 8 +++- .../routes/v1/identity-oidc-auth-router.ts | 6 ++- .../v1/identity-tls-cert-auth-router.ts | 6 ++- .../routes/v1/identity-token-auth-router.ts | 6 ++- .../v1/identity-universal-auth-router.ts | 10 ++++- .../identity-access-token-types.ts | 1 - .../identity-alicloud-auth-service.ts | 31 +++++++++++++-- .../identity-alicloud-auth-types.ts | 1 + .../identity-aws-auth-service.ts | 39 +++++++++++++++++-- .../identity-aws-auth-types.ts | 1 + .../identity-azure-auth-service.ts | 33 ++++++++++++++-- .../identity-azure-auth-types.ts | 1 + .../identity-gcp-auth-service.ts | 32 +++++++++++++-- .../identity-gcp-auth-types.ts | 1 + .../identity-jwt-auth-service.ts | 31 +++++++++++++-- .../identity-jwt-auth-types.ts | 1 + .../identity-kubernetes-auth-service.ts | 31 +++++++++++++-- .../identity-kubernetes-auth-types.ts | 1 + .../identity-ldap-auth-service.ts | 31 +++++++++++++-- .../identity-ldap-auth-types.ts | 1 + .../identity-oci-auth-service.ts | 31 +++++++++++++-- .../identity-oci-auth-types.ts | 1 + .../identity-oidc-auth-service.ts | 31 +++++++++++++-- .../identity-oidc-auth-types.ts | 1 + .../identity-tls-cert-auth-service.ts | 35 +++++++++++++++-- .../identity-tls-cert-auth-types.ts | 1 + .../identity-token-auth-service.ts | 34 ++++++++++++++-- .../identity-token-auth-types.ts | 1 + .../identity-ua/identity-ua-service.ts | 30 +++++++++++++- .../services/identity-ua/identity-ua-types.ts | 7 ++++ .../super-admin/super-admin-service.ts | 3 +- 39 files changed, 457 insertions(+), 71 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 970f9a1a0..de788f936 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -170,10 +170,13 @@ export const IDENTITIES = { } } as const; +const IDENTITY_AUTH_SUB_ORGANIZATION_NAME = "sub-organization slug 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 slug 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.", diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts index 8f64d3b23..70eb9bea6 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -38,6 +38,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi message: "AccessKeyId must be alphanumeric" }) .describe(ALICLOUD_AUTH.LOGIN.AccessKeyId), + subOrganizationName: z.string().trim().optional().describe(ALICLOUD_AUTH.LOGIN.subOrganizationName), SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod), Timestamp: z .string() @@ -74,7 +75,10 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi }, handler: async (req) => { const { identityAliCloudAuth, accessToken, identityAccessToken, identity } = - await server.services.identityAliCloudAuth.login(req.body); + await server.services.identityAliCloudAuth.login({ + ...req.body, + subOrganizationName: req.body.subOrganizationName + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 3cfb19895..152dd0311 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -28,7 +28,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: z.string().trim().optional().describe(AWS_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -41,7 +42,10 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) }, handler: async (req) => { const { identityAwsAuth, accessToken, identityAccessToken, identity } = - await server.services.identityAwsAuth.login(req.body); + await server.services.identityAwsAuth.login({ + ...req.body, + subOrganizationName: req.body.subOrganizationName + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index cdab7af02..e4277777e 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -23,7 +23,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: z.string().trim().optional().describe(AZURE_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index 474999b2b..71f43224e 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -23,7 +23,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: z.string().trim().optional().describe(GCP_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -36,7 +37,10 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) }, handler: async (req) => { const { identityGcpAuth, accessToken, identityAccessToken, identity } = - await server.services.identityGcpAuth.login(req.body); + await server.services.identityGcpAuth.login({ + ...req.body, + subOrganizationName: req.body.subOrganizationName + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index 5d71b3781..3cf79bcfc 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -99,7 +99,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: z.string().trim().optional().describe(JWT_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -114,7 +115,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) const { identityJwtAuth, accessToken, identityAccessToken, identity } = await server.services.identityJwtAuth.login({ identityId: req.body.identityId, - jwt: req.body.jwt + jwt: req.body.jwt, + subOrganizationName: req.body.subOrganizationName }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 28f611aba..9c7b80798 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -44,7 +44,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: z.string().trim().optional().describe(KUBERNETES_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -59,7 +60,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide const { identityKubernetesAuth, accessToken, identityAccessToken, identity } = await server.services.identityKubernetesAuth.login({ identityId: req.body.identityId, - jwt: req.body.jwt + jwt: req.body.jwt, + subOrganizationName: req.body.subOrganizationName }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index dade20ea3..8cf41d0fc 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -124,7 +124,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: z.string().trim().optional().describe(LDAP_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -163,7 +164,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({ diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts index 003d9810b..a243c1a0d 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -40,7 +40,8 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) }); } }) - .describe(OCI_AUTH.LOGIN.headers) + .describe(OCI_AUTH.LOGIN.headers), + subOrganizationName: z.string().trim().optional().describe(OCI_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -53,7 +54,10 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) }, handler: async (req) => { const { identityOciAuth, accessToken, identityAccessToken, identity } = - await server.services.identityOciAuth.login(req.body); + await server.services.identityOciAuth.login({ + ...req.body, + subOrganizationName: req.body.subOrganizationName + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 6fad1f400..6fb66d2af 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -47,7 +47,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: z.string().trim().optional().describe(OIDC_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -62,7 +63,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } = await server.services.identityOidcAuth.login({ identityId: req.body.identityId, - jwt: req.body.jwt + jwt: req.body.jwt, + subOrganizationName: req.body.subOrganizationName }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts index b7a44c62c..9cf97f44c 100644 --- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -46,7 +46,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: z.string().trim().optional().describe(TLS_CERT_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -67,7 +68,8 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid const { identityTlsCertAuth, accessToken, identityAccessToken, identity } = await server.services.identityTlsCertAuth.login({ identityId: req.body.identityId, - clientCertificate: clientCertificate as string + clientCertificate: clientCertificate as string, + subOrganizationName: req.body.subOrganizationName }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index d7cd86330..ca7c2df13 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -307,7 +307,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: z.string().trim().optional().describe(TOKEN_AUTH.CREATE_TOKEN.subOrganizationName) }), response: { 200: z.object({ @@ -328,7 +329,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider actorOrgId: req.permission.orgId, identityId: req.params.identityId, isActorSuperAdmin: isSuperAdmin(req.auth), - ...req.body + ...req.body, + subOrganizationName: req.body.subOrganizationName }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 88a4cb775..63ff18449 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -35,7 +35,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: z.string().trim().optional().describe(UNIVERSAL_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -55,7 +56,12 @@ 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.clientId, + req.body.clientSecret, + req.realIp, + req.body.subOrganizationName + ); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts index b7505810d..e7b73a8c1 100644 --- a/backend/src/services/identity-access-token/identity-access-token-types.ts +++ b/backend/src/services/identity-access-token/identity-access-token-types.ts @@ -7,7 +7,6 @@ export type TIdentityAccessTokenJwtPayload = { clientSecretId: string; identityAccessTokenId: string; authTokenType: string; - subOrganizationId?: string; identityAuth: { oidc?: { claims: Record; diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index fba7fee98..8a8de83cb 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -53,7 +53,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityAliCloudAuthServiceFactory = ReturnType; @@ -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,30 @@ export const identityAliCloudAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { const requestUrl = new URL("https://sts.aliyuncs.com"); @@ -132,7 +156,8 @@ export const identityAliCloudAuthServiceFactory = ({ accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.ALICLOUD_AUTH + authMethod: IdentityAuthMethod.ALICLOUD_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts index 86133491e..575341dd3 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts @@ -11,6 +11,7 @@ export type TLoginAliCloudAuthDTO = { SignatureVersion: string; SignatureNonce: string; Signature: string; + subOrganizationName?: string; }; export type TAttachAliCloudAuthDTO = { diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index b3b6bbfce..a21f86112 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -53,7 +53,7 @@ type TIdentityAwsAuthServiceFactoryDep = { membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityAwsAuthServiceFactory = ReturnType; @@ -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,32 @@ export const identityAwsAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } + try { const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); @@ -207,7 +239,8 @@ export const identityAwsAuthServiceFactory = ({ accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.AWS_AUTH + authMethod: IdentityAuthMethod.AWS_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts index 9844c8a63..4570932f0 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts @@ -5,6 +5,7 @@ export type TLoginAwsAuthDTO = { iamHttpRequestMethod: string; iamRequestBody: string; iamRequestHeaders: string; + subOrganizationName?: string; }; export type TAttachAwsAuthDTO = { diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 3c05511d1..41eea9ae3 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -49,7 +49,7 @@ type TIdentityAzureAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityAzureAuthServiceFactory = ReturnType; @@ -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) { @@ -75,6 +75,32 @@ export const identityAzureAuthServiceFactory = ({ const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } + try { const azureIdentity = await validateAzureIdentity({ tenantId: identityAzureAuth.tenantId, @@ -126,7 +152,8 @@ export const identityAzureAuthServiceFactory = ({ accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.AZURE_AUTH + authMethod: IdentityAuthMethod.AZURE_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts index 485753b6f..78d99dd55 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginAzureAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export type TAttachAzureAuthDTO = { diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 847abd81f..5cf962f13 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -47,7 +47,7 @@ type TIdentityGcpAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityGcpAuthServiceFactory = ReturnType; @@ -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,31 @@ export const identityGcpAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } + try { let gcpIdentityDetails: TGcpIdentityDetails; switch (identityGcpAuth.type) { @@ -166,7 +191,8 @@ export const identityGcpAuthServiceFactory = ({ accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.GCP_AUTH + authMethod: IdentityAuthMethod.GCP_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts index 063630c73..b26fa6539 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginGcpAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export type TAttachGcpAuthDTO = { diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 82935e4a4..2782fad0b 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -60,7 +60,7 @@ type TIdentityJwtAuthServiceFactoryDep = { permissionService: Pick; licenseService: Pick; kmsService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityJwtAuthServiceFactory = ReturnType; @@ -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,30 @@ export const identityJwtAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -246,7 +270,8 @@ export const identityJwtAuthServiceFactory = ({ accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.JWT_AUTH + authMethod: IdentityAuthMethod.JWT_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts index bc19aba83..54d9a928d 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts @@ -49,4 +49,5 @@ export type TRevokeJwtAuthDTO = { export type TLoginJwtAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 212cb0894..78de755ab 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -78,7 +78,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { gatewayV2Service: TGatewayV2ServiceFactory; gatewayDAL: Pick; gatewayV2DAL: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -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,30 @@ export const identityKubernetesAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -487,7 +511,8 @@ export const identityKubernetesAuthServiceFactory = ({ accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.KUBERNETES_AUTH + authMethod: IdentityAuthMethod.KUBERNETES_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index 269fa19e0..decb2957e 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginKubernetesAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export enum IdentityKubernetesAuthTokenReviewMode { diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 455b52412..69c126827 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -70,7 +70,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" >; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -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,30 @@ export const identityLdapAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } const plan = await licenseService.getPlan(identity.orgId); if (!plan.ldap) { throw new BadRequestError({ @@ -204,7 +228,8 @@ export const identityLdapAuthServiceFactory = ({ accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.LDAP_AUTH + authMethod: IdentityAuthMethod.LDAP_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts index a4aea7573..b438c937f 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -59,6 +59,7 @@ export type TGetLdapAuthDTO = { export type TLoginLdapAuthDTO = { identityId: string; + subOrganizationName?: string; }; export type TRevokeLdapAuthDTO = { diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 05e56c77d..44c9f6212 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -51,7 +51,7 @@ type TIdentityOciAuthServiceFactoryDep = { membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityOciAuthServiceFactory = ReturnType; @@ -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,30 @@ export const identityOciAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { @@ -137,7 +161,8 @@ export const identityOciAuthServiceFactory = ({ accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.OCI_AUTH + authMethod: IdentityAuthMethod.OCI_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts index 8eb33a866..0ac043ab8 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts @@ -9,6 +9,7 @@ export type TLoginOciAuthDTO = { "x-date"?: string; date?: string; }; + subOrganizationName?: string; }; export type TAttachOciAuthDTO = { diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index a253c1e95..774078007 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -61,7 +61,7 @@ type TIdentityOidcAuthServiceFactoryDep = { permissionService: Pick; licenseService: Pick; kmsService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; @@ -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,30 @@ export const identityOidcAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -299,7 +323,8 @@ export const identityOidcAuthServiceFactory = ({ accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.OIDC_AUTH + authMethod: IdentityAuthMethod.OIDC_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts index fc5da3e27..5d06d778c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts @@ -38,6 +38,7 @@ export type TGetOidcAuthDTO = { export type TLoginOidcAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export type TRevokeOidcAuthDTO = { diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index 630638a06..678a6a317 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -46,7 +46,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { licenseService: Pick; permissionService: Pick; kmsService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; 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,30 @@ export const identityTlsCertAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -157,7 +185,8 @@ export const identityTlsCertAuthServiceFactory = ({ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.TLS_CERT_AUTH + authMethod: IdentityAuthMethod.TLS_CERT_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts index cf35bb5ee..77932042b 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -4,6 +4,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginTlsCertAuthDTO = { identityId: string; clientCertificate: string; + subOrganizationName?: string; }; export type TAttachTlsCertAuthDTO = { diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index bdc8ab1c1..160f8076c 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -59,7 +59,7 @@ type TIdentityTokenAuthServiceFactoryDep = { >; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityTokenAuthServiceFactory = ReturnType; @@ -424,7 +424,8 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId, name, - isActorSuperAdmin + isActorSuperAdmin, + subOrganizationName }: TCreateTokenAuthTokenDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); @@ -503,6 +504,32 @@ 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 isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } + const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -529,7 +556,8 @@ export const identityTokenAuthServiceFactory = ({ accessTokenNumUses: 0, accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit, name, - authMethod: IdentityAuthMethod.TOKEN_AUTH + authMethod: IdentityAuthMethod.TOKEN_AUTH, + scopeOrgId }, tx ); diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index fdecc6d4c..3074675f2 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -30,6 +30,7 @@ export type TRevokeTokenAuthDTO = { export type TCreateTokenAuthTokenDTO = { identityId: string; name?: string; + subOrganizationName?: string; isActorSuperAdmin?: boolean; } & Omit; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 5ea5c4a6e..1fbc8b197 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -41,6 +41,7 @@ import { TGetUaClientSecretsDTO, TGetUaDTO, TGetUniversalAuthClientSecretByIdDTO, + TLoginUaDTO, TRevokeUaClientSecretDTO, TRevokeUaDTO, TUpdateUaDTO @@ -54,7 +55,7 @@ type TIdentityUaServiceFactoryDep = { membershipIdentityDAL: TMembershipIdentityDALFactory; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; 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,30 @@ export const identityUaServiceFactory = ({ const identity = await identityDAL.findById(identityUa.identityId); const org = await orgDAL.findById(identity.orgId); + const isSubOrg = !!(org.rootOrgId || org.parentOrgId); + + const rootOrgId = isSubOrg ? org.rootOrgId || org.id : org.id; + + // Resolve sub-organization if specified + let scopeOrgId = rootOrgId; + if (subOrganizationName) { + const subOrg = await orgDAL.findOne({ slug: subOrganizationName }); + + if (subOrg) { + if (!isSubOrg || (isSubOrg && subOrg.rootOrgId === rootOrgId)) { + // Verify identity has membership in the sub-organization + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (subOrgMembership) { + scopeOrgId = subOrg.id; + } + } + } + } try { checkIPAgainstBlocklist({ @@ -259,6 +284,7 @@ export const identityUaServiceFactory = ({ accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit, accessTokenPeriod: identityUa.accessTokenPeriod, authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + scopeOrgId, ...accessTokenTTLParams }, tx diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts index 8e7644b58..3ff8f5cc5 100644 --- a/backend/src/services/identity-ua/identity-ua-types.ts +++ b/backend/src/services/identity-ua/identity-ua-types.ts @@ -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; diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 63bab2666..54f1aa3bd 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -640,7 +640,8 @@ export const superAdminServiceFactory = ({ accessTokenNumUses: 0, accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit, name: "Instance Admin Token", - authMethod: IdentityAuthMethod.TOKEN_AUTH + authMethod: IdentityAuthMethod.TOKEN_AUTH, + scopeOrgId: organization.id }, tx );