diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 2956be192..23c6feaca 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -125,6 +125,15 @@ declare module "@fastify/request-context" { namespace: string; name: string; }; + aws?: { + accountId: string; + arn: string; + userId: string; + partition: string; + service: string; + resourceType: string; + resourceName: string; + }; }; identityPermissionMetadata?: Record; // filled by permission service assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string }; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 7b4f5d90c..4fe639510 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -162,6 +162,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { kubernetes: token?.identityAuth?.kubernetes }); } + if (token?.identityAuth?.aws) { + requestContext.set("identityAuthInfo", { + identityId: identity.identityId, + aws: token?.identityAuth?.aws + }); + } break; } case AuthMode.SERVICE_TOKEN: { 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 87adfa5dc..7d393266b 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 @@ -15,5 +15,16 @@ export type TIdentityAccessTokenJwtPayload = { namespace: string; name: string; }; + aws?: { + accountId: string; + arn: string; + userId: string; + + // Derived from ARN + partition: string; // "aws", "aws-gov", "aws-cn" + service: string; // "iam" + resourceType: string; // "user" or "role" + resourceName: string; + }; }; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts index 517e9f613..d0fb4d323 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts @@ -1,67 +1,91 @@ +interface PrincipalArnEntity { + Partition: string; + Service: "iam" | "sts"; + AccountNumber: string; + Type: "user" | "role" | "instance-profile"; + Path: string; + FriendlyName: string; + SessionInfo: string; // Only populated for assumed-role +} + +export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { + // split the ARN into parts using ":" as the delimiter + const fullParts = arn.split(":"); + if (fullParts.length !== 6) { + throw new Error(`Unrecognized ARN: "${arn}" contains ${fullParts.length} colon-separated parts, expected 6`); + } + const [prefix, partition, service, , accountNumber, resource] = fullParts; + if (prefix !== "arn") { + throw new Error(`Unrecognized ARN: "${arn}" does not begin with "arn:"`); + } + + // validate the service is either 'iam' or 'sts' + if (service !== "iam" && service !== "sts") { + throw new Error(`Unrecognized service: "${service}" in ARN "${arn}", expected "iam" or "sts"`); + } + + // parse the last part of the ARN which describes the resource + const parts = resource.split("/"); + if (parts.length < 2) { + throw new Error( + `Unrecognized ARN: "${resource}" in ARN "${arn}" contains fewer than 2 slash-separated parts (expected type/name)` + ); + } + + const [rawType, ...rest] = parts; + + let finalType: PrincipalArnEntity["Type"]; + let friendlyName: string = parts[parts.length - 1]; + let path: string = ""; + let sessionInfo: string = ""; + + // handle different types of resources + switch (rawType) { + case "assumed-role": { + if (rest.length < 2) { + throw new Error( + `Unrecognized ARN: "${resource}" for assumed-role in ARN "${arn}" contains fewer than 3 slash-separated parts (type/roleName/sessionId)` + ); + } + // assumed roles use a special format where the friendly name is the role name + const [roleName, sessionId] = rest; + finalType = "role"; // treat assumed role case as role + friendlyName = roleName; + sessionInfo = sessionId; + break; + } + case "user": + case "role": + case "instance-profile": + finalType = rawType; + path = rest.slice(0, -1).join("/"); + break; + default: + throw new Error( + `Unrecognized principal type: "${rawType}" in ARN "${arn}". Expected "user", "role", "instance-profile", or "assumed-role".` + ); + } + + const entity: PrincipalArnEntity = { + Partition: partition, + Service: service, + AccountNumber: accountNumber, + Type: finalType, + Path: path, + FriendlyName: friendlyName, + SessionInfo: sessionInfo + }; + + return entity; +}; + /** * Extracts the identity ARN from the GetCallerIdentity response to one of the following formats: * - arn:aws:iam::123456789012:user/MyUserName * - arn:aws:iam::123456789012:role/MyRoleName */ export const extractPrincipalArn = (arn: string) => { - // split the ARN into parts using ":" as the delimiter - const fullParts = arn.split(":"); - if (fullParts.length !== 6) { - throw new Error(`Unrecognized ARN: contains ${fullParts.length} colon-separated parts, expected 6`); - } - const [prefix, partition, service, , accountNumber, resource] = fullParts; - if (prefix !== "arn") { - throw new Error('Unrecognized ARN: does not begin with "arn:"'); - } - - // structure to hold the parsed data - const entity = { - Partition: partition, - Service: service, - AccountNumber: accountNumber, - Type: "", - Path: "", - FriendlyName: "", - SessionInfo: "" - }; - - // validate the service is either 'iam' or 'sts' - if (entity.Service !== "iam" && entity.Service !== "sts") { - throw new Error(`Unrecognized service: ${entity.Service}, not one of iam or sts`); - } - - // parse the last part of the ARN which describes the resource - const parts = resource.split("/"); - if (parts.length < 2) { - throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 2 slash-separated parts`); - } - - const [type, ...rest] = parts; - entity.Type = type; - entity.FriendlyName = parts[parts.length - 1]; - - // handle different types of resources - switch (entity.Type) { - case "assumed-role": { - if (rest.length < 2) { - throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 3 slash-separated parts`); - } - // assumed roles use a special format where the friendly name is the role name - const [roleName, sessionId] = rest; - entity.Type = "role"; // treat assumed role case as role - entity.FriendlyName = roleName; - entity.SessionInfo = sessionId; - break; - } - case "user": - case "role": - case "instance-profile": - // standard cases: just join back the path if there's any - entity.Path = rest.slice(0, -1).join("/"); - break; - default: - throw new Error(`Unrecognized principal type: "${entity.Type}"`); - } + const entity = extractPrincipalArnEntity(arn); return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; }; 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 7c339f15e..b5035946e 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 @@ -22,7 +22,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal"; -import { extractPrincipalArn } from "./identity-aws-auth-fns"; +import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns"; import { TAttachAwsAuthDTO, TAwsGetCallerIdentityHeaders, @@ -107,7 +107,7 @@ export const identityAwsAuthServiceFactory = ({ const { data: { GetCallerIdentityResponse: { - GetCallerIdentityResult: { Account, Arn } + GetCallerIdentityResult: { Account, Arn, UserId } } } }: { data: TGetCallerIdentityResponse } = await axios({ @@ -168,11 +168,25 @@ export const identityAwsAuthServiceFactory = ({ }); const appCfg = getConfig(); + const splitArn = extractPrincipalArnEntity(Arn); const accessToken = crypto.jwt().sign( { identityId: identityAwsAuth.identityId, identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + aws: { + accountId: Account, + arn: Arn, + userId: UserId, + + // Derived from ARN + partition: splitArn.Partition, + service: splitArn.Service, + resourceType: splitArn.Type, + resourceName: splitArn.FriendlyName + } + } } as TIdentityAccessTokenJwtPayload, appCfg.AUTH_SECRET, // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error diff --git a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx index b953a80bf..20da1a989 100644 --- a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx +++ b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx @@ -30,10 +30,10 @@ For methods like OIDC, these come as claims in the token and can be made availab - 1. Navigate to the Identity Authentication settings and select the OIDC Auth Method. - 2. In the **Advanced section**, locate the Claim Mapping configuration. - 3. Map the OIDC claims to permission attributes by specifying: - - **Attribute Name:** The identifier to be used in your policies (e.g., department). + 1. Navigate to the Identity Authentication settings and select the OIDC Auth Method. + 2. In the **Advanced section**, locate the Claim Mapping configuration. + 3. Map the OIDC claims to permission attributes by specifying: + - **Attribute Name:** The identifier to be used in your policies (e.g., department). - **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department). For example, if your OIDC provider returns: @@ -74,6 +74,21 @@ For methods like OIDC, these come as claims in the token and can be made availab + + For identities authenticated using AWS Auth, several attributes can be accessed: + + ``` + {{ identity.auth.aws.accountId }} + {{ identity.auth.aws.arn }} + {{ identity.auth.aws.userId }} + {{ identity.auth.aws.partition }} + {{ identity.auth.aws.service }} + {{ identity.auth.aws.resourceType }} + {{ identity.auth.aws.resourceName }} + ``` + + + At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible. diff --git a/docs/images/platform/access-controls/abac-policy-aws-format.png b/docs/images/platform/access-controls/abac-policy-aws-format.png new file mode 100644 index 000000000..4b00c0513 Binary files /dev/null and b/docs/images/platform/access-controls/abac-policy-aws-format.png differ