diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 9f825b6b6..325da75e1 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -65,6 +65,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p import { THsmServiceFactory } from "@app/services/hsm/hsm-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; +import { TIdentityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service"; import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service"; import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service"; @@ -218,6 +219,7 @@ declare module "fastify" { identityUa: TIdentityUaServiceFactory; identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory; + identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; identityOciAuth: TIdentityOciAuthServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8fd073a49..1d4ad1797 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -125,6 +125,9 @@ import { TIdentityAccessTokens, TIdentityAccessTokensInsert, TIdentityAccessTokensUpdate, + TIdentityAlicloudAuths, + TIdentityAlicloudAuthsInsert, + TIdentityAlicloudAuthsUpdate, TIdentityAwsAuths, TIdentityAwsAuthsInsert, TIdentityAwsAuthsUpdate, @@ -786,6 +789,11 @@ declare module "knex/types/tables" { TIdentityGcpAuthsInsert, TIdentityGcpAuthsUpdate >; + [TableName.IdentityAliCloudAuth]: KnexOriginal.CompositeTableType< + TIdentityAlicloudAuths, + TIdentityAlicloudAuthsInsert, + TIdentityAlicloudAuthsUpdate + >; [TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType< TIdentityAwsAuths, TIdentityAwsAuthsInsert, diff --git a/backend/src/db/migrations/20250610205158_alicloud-machine-identity.ts b/backend/src/db/migrations/20250610205158_alicloud-machine-identity.ts new file mode 100644 index 000000000..43832dc23 --- /dev/null +++ b/backend/src/db/migrations/20250610205158_alicloud-machine-identity.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityAliCloudAuth))) { + await knex.schema.createTable(TableName.IdentityAliCloudAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + t.timestamps(true, true, true); + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.string("type").notNullable(); + + t.string("allowedArns").notNullable(); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityAliCloudAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityAliCloudAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityAliCloudAuth); +} diff --git a/backend/src/db/schemas/identity-alicloud-auths.ts b/backend/src/db/schemas/identity-alicloud-auths.ts new file mode 100644 index 000000000..37950d3cf --- /dev/null +++ b/backend/src/db/schemas/identity-alicloud-auths.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityAlicloudAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + createdAt: z.date(), + updatedAt: z.date(), + identityId: z.string().uuid(), + type: z.string(), + allowedArns: z.string() +}); + +export type TIdentityAlicloudAuths = z.infer; +export type TIdentityAlicloudAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityAlicloudAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 7a27caf9e..292551c80 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -39,6 +39,7 @@ export * from "./group-project-memberships"; export * from "./groups"; export * from "./identities"; export * from "./identity-access-tokens"; +export * from "./identity-alicloud-auths"; export * from "./identity-aws-auths"; export * from "./identity-azure-auths"; export * from "./identity-gcp-auths"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index df0a858b9..ceba6e370 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -80,6 +80,7 @@ export enum TableName { IdentityGcpAuth = "identity_gcp_auths", IdentityAzureAuth = "identity_azure_auths", IdentityUaClientSecret = "identity_ua_client_secrets", + IdentityAliCloudAuth = "identity_alicloud_auths", IdentityAwsAuth = "identity_aws_auths", IdentityOciAuth = "identity_oci_auths", IdentityOidcAuth = "identity_oidc_auths", @@ -247,6 +248,7 @@ export enum IdentityAuthMethod { UNIVERSAL_AUTH = "universal-auth", KUBERNETES_AUTH = "kubernetes-auth", GCP_AUTH = "gcp-auth", + ALICLOUD_AUTH = "alicloud-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", OCI_AUTH = "oci-auth", diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 1b07982ca..87a98305f 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -170,6 +170,12 @@ export enum EventType { REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth", GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth", + LOGIN_IDENTITY_ALICLOUD_AUTH = "login-identity-alicloud-auth", + ADD_IDENTITY_ALICLOUD_AUTH = "add-identity-alicloud-auth", + UPDATE_IDENTITY_ALICLOUD_AUTH = "update-identity-alicloud-auth", + REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth", + GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth", + LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth", ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth", UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth", @@ -1060,6 +1066,53 @@ interface GetIdentityAwsAuthEvent { }; } +interface LoginIdentityAliCloudAuthEvent { + type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH; + metadata: { + identityId: string; + identityAliCloudAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityAliCloudAuthEvent { + type: EventType.ADD_IDENTITY_ALICLOUD_AUTH; + metadata: { + identityId: string; + allowedArns: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface DeleteIdentityAliCloudAuthEvent { + type: EventType.REVOKE_IDENTITY_ALICLOUD_AUTH; + metadata: { + identityId: string; + }; +} + +interface UpdateIdentityAliCloudAuthEvent { + type: EventType.UPDATE_IDENTITY_ALICLOUD_AUTH; + metadata: { + identityId: string; + allowedArns: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityAliCloudAuthEvent { + type: EventType.GET_IDENTITY_ALICLOUD_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityOciAuthEvent { type: EventType.LOGIN_IDENTITY_OCI_AUTH; metadata: { @@ -3272,6 +3325,11 @@ export type Event = | UpdateIdentityAwsAuthEvent | GetIdentityAwsAuthEvent | DeleteIdentityAwsAuthEvent + | LoginIdentityAliCloudAuthEvent + | AddIdentityAliCloudAuthEvent + | UpdateIdentityAliCloudAuthEvent + | GetIdentityAliCloudAuthEvent + | DeleteIdentityAliCloudAuthEvent | LoginIdentityOciAuthEvent | AddIdentityOciAuthEvent | UpdateIdentityOciAuthEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index caac30556..0670c9333 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -21,6 +21,7 @@ export enum ApiDocsTags { TokenAuth = "Token Auth", UniversalAuth = "Universal Auth", GcpAuth = "GCP Auth", + AliCloudAuth = "Alibaba Cloud Auth", AwsAuth = "AWS Auth", OciAuth = "OCI Auth", AzureAuth = "Azure Auth", @@ -243,6 +244,43 @@ export const LDAP_AUTH = { } } as const; +export const ALICLOUD_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login.", + Action: "The Alibaba Cloud API action. For STS GetCallerIdentity, this should be 'GetCallerIdentity'.", + Format: "The response format. For STS GetCallerIdentity, this should be 'JSON'.", + Version: "The API version. This should be in 'YYYY-MM-DD' format (e.g., '2015-04-01').", + AccessKeyId: "The AccessKey ID of the RAM user or STS token.", + SignatureMethod: "The signature algorithm. For STS GetCallerIdentity, this should be 'HMAC-SHA1'.", + 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." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.", + accessTokenTTL: "The lifetime for an access token in seconds.", + accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." + }, + UPDATE: { + identityId: "The ID of the identity to update the auth method for.", + allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.", + accessTokenTTL: "The new lifetime for an access token in seconds.", + accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", + accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." + }, + RETRIEVE: { + identityId: "The ID of the identity to retrieve the auth method for." + }, + REVOKE: { + identityId: "The ID of the identity to revoke the auth method for." + } +} as const; + export const AWS_AUTH = { LOGIN: { identityId: "The ID of the identity to login.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5513fb49b..262e8f373 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -172,6 +172,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { identityServiceFactory } from "@app/services/identity/identity-service"; import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; +import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal"; +import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service"; import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal"; import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal"; @@ -383,6 +385,7 @@ export const registerRoutes = async ( const identityUaDAL = identityUaDALFactory(db); const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); + const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityOciAuthDAL = identityOciAuthDALFactory(db); @@ -1482,6 +1485,14 @@ export const registerRoutes = async ( licenseService }); + const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ + identityAccessTokenDAL, + identityAliCloudAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService + }); + const identityAwsAuthService = identityAwsAuthServiceFactory({ identityAccessTokenDAL, identityAwsAuthDAL, @@ -1931,6 +1942,7 @@ export const registerRoutes = async ( identityUa: identityUaService, identityKubernetesAuth: identityKubernetesAuthService, identityGcpAuth: identityGcpAuthService, + identityAliCloudAuth: identityAliCloudAuthService, identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, identityOciAuth: identityOciAuthService, diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts new file mode 100644 index 000000000..a9b2d9b03 --- /dev/null +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -0,0 +1,381 @@ +import RE2 from "re2"; +import { z } from "zod"; + +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 { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { validateArns } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; + +export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/alicloud-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.AliCloudAuth], + description: "Login with Alibaba Cloud Auth", + body: z.object({ + identityId: z.string().trim().describe(ALICLOUD_AUTH.LOGIN.identityId), + Action: z.enum(["GetCallerIdentity"]).describe(ALICLOUD_AUTH.LOGIN.Action), + Format: z.enum(["JSON"]).describe(ALICLOUD_AUTH.LOGIN.Format), + Version: z + .string() + .refine((val) => new RE2("^\\d{4}-\\d{2}-\\d{2}$").test(val), { + message: "Version must be in YYYY-MM-DD format" + }) + .describe(ALICLOUD_AUTH.LOGIN.Version), + AccessKeyId: z + .string() + .refine((val) => new RE2("^[A-Za-z0-9]+$").test(val), { + message: "AccessKeyId must be alphanumeric" + }) + .describe(ALICLOUD_AUTH.LOGIN.AccessKeyId), + SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod), + Timestamp: z + .string() + .datetime({ + message: "Timestamp must be in YYYY-MM-DDTHH:mm:ssZ format" + }) + .refine((val) => val.endsWith("Z"), { + message: "Timestamp must be in YYYY-MM-DDTHH:mm:ssZ format" + }) + .describe(ALICLOUD_AUTH.LOGIN.Timestamp), + SignatureVersion: z.enum(["1.0"]).describe(ALICLOUD_AUTH.LOGIN.SignatureVersion), + SignatureNonce: z + .string() + .refine((val) => new RE2("^[a-zA-Z0-9-_.]+$").test(val), { + message: + "SignatureNonce must be at least 1 character long and contain only URL-safe characters (alphanumeric, -, _, .)" + }) + .describe(ALICLOUD_AUTH.LOGIN.SignatureNonce), + Signature: z + .string() + .refine((val) => new RE2("^[A-Za-z0-9+/=]+$").test(val), { + message: "Signature must be base64 characters" + }) + .describe(ALICLOUD_AUTH.LOGIN.Signature) + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityAliCloudAuth.login(req.body); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH, + metadata: { + identityId: identityAliCloudAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityAliCloudAuthId: identityAliCloudAuth.id + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityAliCloudAuth.accessTokenTTL, + accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/alicloud-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.AliCloudAuth], + description: "Attach Alibaba Cloud Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(ALICLOUD_AUTH.ATTACH.identityId) + }), + body: z + .object({ + allowedArns: validateArns.describe(ALICLOUD_AUTH.ATTACH.allowedArns), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(ALICLOUD_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(ALICLOUD_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(ALICLOUD_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(ALICLOUD_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + response: { + 200: z.object({ + identityAliCloudAuth: IdentityAlicloudAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAliCloudAuth = await server.services.identityAliCloudAuth.attachAliCloudAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId, + isActorSuperAdmin: isSuperAdmin(req.auth) + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAliCloudAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_ALICLOUD_AUTH, + metadata: { + identityId: identityAliCloudAuth.identityId, + allowedArns: identityAliCloudAuth.allowedArns, + accessTokenTTL: identityAliCloudAuth.accessTokenTTL, + accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityAliCloudAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityAliCloudAuth }; + } + }); + + server.route({ + method: "PATCH", + url: "/alicloud-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.AliCloudAuth], + description: "Update Alibaba Cloud Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(ALICLOUD_AUTH.UPDATE.identityId) + }), + body: z + .object({ + allowedArns: validateArns.describe(ALICLOUD_AUTH.UPDATE.allowedArns), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(ALICLOUD_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .optional() + .describe(ALICLOUD_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .optional() + .describe(ALICLOUD_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(ALICLOUD_AUTH.UPDATE.accessTokenMaxTTL) + }) + .refine( + (val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true), + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + response: { + 200: z.object({ + identityAliCloudAuth: IdentityAlicloudAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAliCloudAuth = await server.services.identityAliCloudAuth.updateAliCloudAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId, + allowedArns: req.body.allowedArns + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAliCloudAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_ALICLOUD_AUTH, + metadata: { + identityId: identityAliCloudAuth.identityId, + allowedArns: identityAliCloudAuth.allowedArns, + accessTokenTTL: identityAliCloudAuth.accessTokenTTL, + accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityAliCloudAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityAliCloudAuth }; + } + }); + + server.route({ + method: "GET", + url: "/alicloud-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.AliCloudAuth], + description: "Retrieve Alibaba Cloud Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(ALICLOUD_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityAliCloudAuth: IdentityAlicloudAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAliCloudAuth = await server.services.identityAliCloudAuth.getAliCloudAuth({ + identityId: req.params.identityId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAliCloudAuth.orgId, + event: { + type: EventType.GET_IDENTITY_ALICLOUD_AUTH, + metadata: { + identityId: identityAliCloudAuth.identityId + } + } + }); + return { identityAliCloudAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/alicloud-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.AliCloudAuth], + description: "Delete Alibaba Cloud Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(ALICLOUD_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityAliCloudAuth: IdentityAlicloudAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAliCloudAuth = await server.services.identityAliCloudAuth.revokeIdentityAliCloudAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAliCloudAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_ALICLOUD_AUTH, + metadata: { + identityId: identityAliCloudAuth.identityId + } + } + }); + + return { identityAliCloudAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 76cf8761f..2363147b6 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -15,6 +15,7 @@ import { registerCertRouter } from "./certificate-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; +import { registerIdentityAliCloudAuthRouter } from "./identity-alicloud-auth-router"; import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router"; import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; @@ -63,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityKubernetesRouter); await authRouter.register(registerIdentityGcpAuthRouter); await authRouter.register(registerIdentityAccessTokenRouter); + await authRouter.register(registerIdentityAliCloudAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityOciAuthRouter); diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index fea12d3ee..879ca9fd3 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -28,6 +28,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { `${TableName.IdentityUniversalAuth}.id` ) .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`) .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`) .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`) @@ -44,6 +49,10 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .select( db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"), + db + .ref("accessTokenTrustedIps") + .withSchema(TableName.IdentityAliCloudAuth) + .as("accessTokenTrustedIpsAliCloud"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), @@ -62,6 +71,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { ...doc, trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa, trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp, + trustedIpsAliCloudAuth: doc.accessTokenTrustedIpsAliCloud, trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws, trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index c5b57373d..7c8944f50 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -193,6 +193,7 @@ export const identityAccessTokenServiceFactory = ({ const trustedIpsMap: Record = { [IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth, [IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth, + [IdentityAuthMethod.ALICLOUD_AUTH]: identityAccessToken.trustedIpsAliCloudAuth, [IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth, [IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth, [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-dal.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-dal.ts new file mode 100644 index 000000000..a4ca50766 --- /dev/null +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-dal.ts @@ -0,0 +1,9 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityAliCloudAuthDALFactory = ReturnType; + +export const identityAliCloudAuthDALFactory = (db: TDbClient) => { + return ormify(db, TableName.IdentityAliCloudAuth); +}; 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 new file mode 100644 index 000000000..ad357b4e9 --- /dev/null +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -0,0 +1,361 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ForbiddenError } from "@casl/ability"; +import { AxiosError } from "axios"; +import jwt from "jsonwebtoken"; + +import { IdentityAuthMethod } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { logger } from "@app/lib/logger"; + +import { ActorType, AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; +import { TIdentityAliCloudAuthDALFactory } from "./identity-alicloud-auth-dal"; +import { + TAliCloudGetUserResponse, + TAttachAliCloudAuthDTO, + TGetAliCloudAuthDTO, + TLoginAliCloudAuthDTO, + TRevokeAliCloudAuthDTO, + TUpdateAliCloudAuthDTO +} from "./identity-alicloud-auth-types"; + +type TIdentityAliCloudAuthServiceFactoryDep = { + identityAccessTokenDAL: Pick; + identityAliCloudAuthDAL: Pick< + TIdentityAliCloudAuthDALFactory, + "findOne" | "transaction" | "create" | "updateById" | "delete" + >; + identityOrgMembershipDAL: Pick; + licenseService: Pick; + permissionService: Pick; +}; + +export type TIdentityAliCloudAuthServiceFactory = ReturnType; + +export const identityAliCloudAuthServiceFactory = ({ + identityAccessTokenDAL, + identityAliCloudAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService +}: TIdentityAliCloudAuthServiceFactoryDep) => { + const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { + const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); + if (!identityAliCloudAuth) { + throw new NotFoundError({ + message: "Alibaba Cloud auth method not found for identity, did you configure Alibaba Cloud auth?" + }); + } + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ + identityId: identityAliCloudAuth.identityId + }); + + const requestUrl = new URL("https://sts.aliyuncs.com"); + + for (const key of Object.keys(params)) { + requestUrl.searchParams.set(key, (params as Record)[key]); + } + + const { data } = await request.get(requestUrl.toString()).catch((err: AxiosError) => { + logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud"); + throw err; + }); + + if (identityAliCloudAuth.allowedArns) { + // In the future we could do partial checks for role ARNs + const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn); + + if (!isAccountAllowed) + throw new UnauthorizedError({ + message: "Access denied: Alibaba Cloud account ARN not allowed." + }); + } + + // Generate the token + const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityAliCloudAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityAliCloudAuth.accessTokenTTL, + accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.ALICLOUD_AUTH + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityAliCloudAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + return { + identityAliCloudAuth, + accessToken, + identityAccessToken, + identityMembershipOrg + }; + }; + + const attachAliCloudAuth = async ({ + identityId, + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId, + isActorSuperAdmin + }: TAttachAliCloudAuthDTO) => { + await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { + throw new BadRequestError({ + message: "Failed to add Alibaba Cloud Auth to already configured identity" + }); + } + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const identityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => { + const doc = await identityAliCloudAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + type: "iam", + allowedArns, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + return doc; + }); + return { ...identityAliCloudAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateAliCloudAuth = async ({ + identityId, + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateAliCloudAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { + throw new NotFoundError({ + message: "The identity does not have Alibaba Cloud Auth attached" + }); + } + + const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityAliCloudAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityAliCloudAuth.accessTokenTTL) > + (accessTokenMaxTTL || identityAliCloudAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const updatedAliCloudAuth = await identityAliCloudAuthDAL.updateById(identityAliCloudAuth.id, { + allowedArns, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { ...updatedAliCloudAuth, orgId: identityMembershipOrg.orgId }; + }; + + const getAliCloudAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAliCloudAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have Alibaba Cloud Auth attached" + }); + } + + const alicloudIdentityAuth = await identityAliCloudAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + return { ...alicloudIdentityAuth, orgId: identityMembershipOrg.orgId }; + }; + + const revokeIdentityAliCloudAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeAliCloudAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); + if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.ALICLOUD_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have Alibaba Cloud auth" + }); + } + const { permission, membership } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + + const { permission: rolePermission } = await permissionService.getOrgPermission( + ActorType.IDENTITY, + identityMembershipOrg.identityId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + + const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to revoke Alibaba Cloud auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const revokedIdentityAliCloudAuth = await identityAliCloudAuthDAL.transaction(async (tx) => { + const deletedAliCloudAuth = await identityAliCloudAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.ALICLOUD_AUTH }, tx); + + return { ...deletedAliCloudAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityAliCloudAuth; + }; + + return { + login, + attachAliCloudAuth, + updateAliCloudAuth, + getAliCloudAuth, + revokeIdentityAliCloudAuth + }; +}; 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 new file mode 100644 index 000000000..86133491e --- /dev/null +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts @@ -0,0 +1,45 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TLoginAliCloudAuthDTO = { + identityId: string; + Action: string; + Format: string; + Version: string; + AccessKeyId: string; + SignatureMethod: string; + Timestamp: string; + SignatureVersion: string; + SignatureNonce: string; + Signature: string; +}; + +export type TAttachAliCloudAuthDTO = { + identityId: string; + allowedArns: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; +} & Omit; + +export type TUpdateAliCloudAuthDTO = { + identityId: string; + allowedArns: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetAliCloudAuthDTO = { + identityId: string; +} & Omit; + +export type TRevokeAliCloudAuthDTO = { + identityId: string; +} & Omit; + +export type TAliCloudGetUserResponse = { + Arn: string; +}; diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-validators.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-validators.ts new file mode 100644 index 000000000..80fcc444b --- /dev/null +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-validators.ts @@ -0,0 +1,25 @@ +import RE2 from "re2"; +import { z } from "zod"; + +const arnSchema = z + .string() + .refine( + (val) => new RE2("^acs:ram::[0-9]{16}:(user|role)/.*$").test(val), + "Invalid ARN format. Expected format: acs:ram::[0-9]{16}:(user|role)/*" + ); +export const validateArns = z + .string() + .trim() + .min(1, "Allowed ARNs required") + .max(500, "Input exceeds the maximum limit of 500 characters") + .transform((val) => { + if (!val) return []; + return val + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + }) + .refine((arr) => arr.every((name) => arnSchema.safeParse(name).success), { + message: "One or more ARNs are invalid" + }) + .transform((arr) => arr.join(", ")); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index 4928fd178..433f5ebd9 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -4,6 +4,7 @@ import { TDbClient } from "@app/db"; import { TableName, TIdentities, + TIdentityAlicloudAuths, TIdentityAwsAuths, TIdentityAzureAuths, TIdentityGcpAuths, @@ -57,6 +58,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityGcpAuth}.identityId` ) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) .leftJoin( TableName.IdentityAwsAuth, `${TableName.IdentityProjectMembership}.identityId`, @@ -111,6 +117,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("type").as("projectType").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), @@ -267,6 +274,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId` ) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) .leftJoin( TableName.IdentityAwsAuth, `${TableName.Identity}.id`, @@ -319,6 +331,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("name").as("projectName").withSchema(TableName.Project), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), @@ -346,6 +359,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { identityId, identityName, uaId, + alicloudId, awsId, gcpId, kubernetesId, @@ -367,6 +381,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { name: identityName, authMethods: buildAuthMethods({ uaId, + alicloudId, awsId, gcpId, kubernetesId, diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 3fa2482aa..3020d9c47 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -3,6 +3,7 @@ import { IdentityAuthMethod } from "@app/db/schemas"; export const buildAuthMethods = ({ uaId, gcpId, + alicloudId, awsId, kubernetesId, ociId, @@ -14,6 +15,7 @@ export const buildAuthMethods = ({ }: { uaId?: string; gcpId?: string; + alicloudId?: string; awsId?: string; kubernetesId?: string; ociId?: string; @@ -26,6 +28,7 @@ export const buildAuthMethods = ({ return [ ...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null], ...[gcpId ? IdentityAuthMethod.GCP_AUTH : null], + ...[alicloudId ? IdentityAuthMethod.ALICLOUD_AUTH : null], ...[awsId ? IdentityAuthMethod.AWS_AUTH : null], ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], ...[ociId ? IdentityAuthMethod.OCI_AUTH : null], diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index af5537249..7cf51b9d8 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -3,6 +3,7 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName, + TIdentityAlicloudAuths, TIdentityAwsAuths, TIdentityAzureAuths, TIdentityGcpAuths, @@ -53,6 +54,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityGcpAuth}.identityId` ) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) .leftJoin( TableName.IdentityAwsAuth, `${TableName.IdentityOrgMembership}.identityId`, @@ -99,6 +105,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), @@ -183,6 +190,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { "paginatedIdentity.identityId", `${TableName.IdentityGcpAuth}.identityId` ) + .leftJoin( + TableName.IdentityAliCloudAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityAliCloudAuth}.identityId` + ) .leftJoin( TableName.IdentityAwsAuth, "paginatedIdentity.identityId", @@ -236,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), @@ -278,6 +291,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { id, orgId, uaId, + alicloudId, awsId, gcpId, jwtId, @@ -312,6 +326,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { name: identityName, authMethods: buildAuthMethods({ uaId, + alicloudId, awsId, gcpId, kubernetesId, @@ -459,6 +474,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), @@ -502,6 +518,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { total_count, id, uaId, + alicloudId, awsId, gcpId, jwtId, @@ -536,6 +553,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { name: identityName, authMethods: buildAuthMethods({ uaId, + alicloudId, awsId, gcpId, kubernetesId, diff --git a/docs/api-reference/endpoints/alicloud-auth/attach.mdx b/docs/api-reference/endpoints/alicloud-auth/attach.mdx new file mode 100644 index 000000000..1e21eb749 --- /dev/null +++ b/docs/api-reference/endpoints/alicloud-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/alicloud-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/alicloud-auth/login.mdx b/docs/api-reference/endpoints/alicloud-auth/login.mdx new file mode 100644 index 000000000..511778200 --- /dev/null +++ b/docs/api-reference/endpoints/alicloud-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/alicloud-auth/login" +--- diff --git a/docs/api-reference/endpoints/alicloud-auth/retrieve.mdx b/docs/api-reference/endpoints/alicloud-auth/retrieve.mdx new file mode 100644 index 000000000..b63f9c746 --- /dev/null +++ b/docs/api-reference/endpoints/alicloud-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/alicloud-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/alicloud-auth/revoke.mdx b/docs/api-reference/endpoints/alicloud-auth/revoke.mdx new file mode 100644 index 000000000..74349ab0a --- /dev/null +++ b/docs/api-reference/endpoints/alicloud-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/alicloud-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/alicloud-auth/update.mdx b/docs/api-reference/endpoints/alicloud-auth/update.mdx new file mode 100644 index 000000000..8295658ba --- /dev/null +++ b/docs/api-reference/endpoints/alicloud-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/alicloud-auth/identities/{identityId}" +--- diff --git a/docs/documentation/platform/identities/alicloud-auth.mdx b/docs/documentation/platform/identities/alicloud-auth.mdx new file mode 100644 index 000000000..2f54ef71b --- /dev/null +++ b/docs/documentation/platform/identities/alicloud-auth.mdx @@ -0,0 +1,192 @@ +--- +title: Alibaba Cloud Auth +description: "Learn how to authenticate with Infisical using Alibaba Cloud user accounts." +--- + +**Alibaba Cloud Auth** is an authentication method that verifies Alibaba Cloud users through signature validation, allowing secure access to Infisical resources. + +## Diagram + +The following sequence diagram illustrates the Alibaba Cloud Auth workflow for authenticating Alibaba Cloud users with Infisical. + +```mermaid +sequenceDiagram + participant Client + participant Infisical + participant Alibaba Cloud + + Note over Client,Client: Step 1: Sign user identity request + + Note over Client,Infisical: Step 2: Login Operation + Client->>Infisical: Send signed request details to /api/v1/auth/alicloud-auth/login + + Note over Infisical,Alibaba Cloud: Step 3: Request verification + Infisical->>Alibaba Cloud: Forward signed request + Alibaba Cloud-->>Infisical: Return user details + + Note over Infisical: Step 4: Identity property validation + Infisical->>Client: Return short-lived access token + + Note over Client,Infisical: Step 5: Access Infisical API with token + Client->>Infisical: Make authenticated requests using the short-lived access token +``` + +## Concept + +At a high level, Infisical authenticates an Alibaba Cloud user by verifying its identity and checking that it meets specific requirements (e.g., its ARN is whitelisted) at the `/api/v1/auth/alicloud-auth/login` endpoint. If successful, +then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. + +To be more specific: +1. The client signs a `GetCallerIdentity` request using an Alibaba Cloud user's access key secret; this is done using an HMAC sha1 algorithm. +2. The client sends the signed request information alongside the signature to Infisical at the `/api/v1/auth/alicloud-auth/login` endpoint. +3. Infisical reconstructs the request and sends it to Alibaba Cloud for verification and obtains the identity associated with the Alibaba Cloud user. +4. Infisical checks the user's properties against set criteria such as **Allowed ARNs**. +5. If all checks pass, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API. + +## Prerequisite + +In order to sign requests, you must have an Alibaba Cloud user with credentials such as access key ID and secret. If you're unaware of how to create a user and obtain the needed credentials, expand the menu below. + + + + + Visit https://ram.console.aliyun.com/users to get to the Users page and click **Create User**. + + ![Users Page](/images/platform/identities/alicloud/users-page.png) + + + Fill out the username and display name with values of your choice and click **OK**. + + ![User Info](/images/platform/identities/alicloud/user-info.png) + + + After a user has been created, click on its row to see user information. + + ![User Info](/images/platform/identities/alicloud/user-row.png) + + + Click **Create AccessKey** and select the most relevant option for your use-case. Then click **Continue**. + + ![User Info](/images/platform/identities/alicloud/create-access-key.png) + + + Save the displayed credentials for later steps. + + ![User Info](/images/platform/identities/alicloud/credentials.png) + + + + +## Guide + +In the following steps, we explore how to create and use identities for your workloads and applications on Alibaba Cloud to +access the Infisical API using request signing. + +### Creating an identity + +To create an identity, head to your Organization Settings > Access Control > [Identities](https://app.infisical.com/organization/access-management?selectedTab=identities) and press **Create identity**. + +![identities organization](/images/platform/identities/identities-org.png) + +When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles). + +![identities organization create](/images/platform/identities/identities-org-create.png) + +Input some details for your new identity: +- **Name (required):** A friendly name for the identity. +- **Role (required):** A role from the [**Organization Roles**](https://app.infisical.com/organization/access-management?selectedTab=roles) tab for the identity to assume. The organization role assigned will determine what organization-level resources this identity can have access to. + +Once you've created an identity, you'll be redirected to a page where you can manage the identity. + +![identities page](/images/platform/identities/identities-page.png) + +Since the identity has been configured with [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) by default, you should reconfigure it to use Alibaba Cloud Auth instead. To do this, click the cog next to **Universal Auth** and then select **Delete** in the options dropdown. + +![identities press cog](/images/platform/identities/identities-press-cog.png) + +![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png) + +Now create a new Alibaba Cloud Auth Method. + +![identities create alicloud auth method](/images/platform/identities/alicloud/create-auth-method.png) + +Here's some information about each field: +- **Allowed ARNs:** A comma-separated list of trusted Alibaba Cloud ARNs that are allowed to authenticate with Infisical. +- **Access Token TTL (default is `2592000` equivalent to 30 days):** The lifetime for an access token in seconds. This value will be referenced at renewal time. +- **Access Token Max TTL (default is `2592000` equivalent to 30 days):** The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. +- **Access Token Max Number of Uses (default is `0`):** The maximum number of times that an access token can be used; a value of `0` implies an infinite number of uses. +- **Access Token Trusted IPs:** The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. + +### Adding an identity to a project + +In order to allow an identity to access project-level resources such as secrets, you must add it to the relevant projects. + +To do this, head over to the project you want to add the identity to and navigate to Project Settings > Access Control > Machine Identities and press **Add Identity**. + +![identities project](/images/platform/identities/identities-project.png) + +Select the identity you want to add to the project and the project-level role you want it to assume. The project role given to the identity will determine what project-level resources this identity can access. + +![identities project create](/images/platform/identities/identities-project-create.png) + +### Accessing the Infisical API with the identity + +To access the Infisical API as the identity, you need to construct a signed `GetCallerIdentity` request and then make a request to the `/api/v1/auth/alicloud-auth/login` endpoint passing the signed data and signature. + +Below is an example of how you can authenticate with Infisical using NodeJS. + +```ts +import crypto from "crypto"; + +// We highly recommend using environment variables instead of hardcoding these values +const ALICLOUD_ACCESS_KEY_ID = "..."; +const ALICLOUD_ACCESS_KEY_SECRET = "..."; + +const params: { [key: string]: string } = { + Action: "GetCallerIdentity", + Format: "JSON", + Version: "2015-04-01", + AccessKeyId: ALICLOUD_ACCESS_KEY_ID, + SignatureMethod: "HMAC-SHA1", + Timestamp: new Date().toISOString(), + SignatureVersion: "1.0", + SignatureNonce: crypto.randomBytes(16).toString("hex"), +}; + +const canonicalizedQueryString = Object.keys(params) + .sort() + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join("&"); + +const stringToSign = `GET&%2F&${encodeURIComponent(canonicalizedQueryString)}`; + +const signature = crypto + .createHmac("sha1", `${ALICLOUD_ACCESS_KEY_SECRET}&`) + .update(stringToSign) + .digest("base64"); + +const res = await fetch( + "https://app.infisical.com/api/v1/auth/alicloud-auth/login", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + identityId: "...", // Replace with your identity ID + Signature: signature, + ...params, + }), + }, +); + +const json = await res.json(); + +console.log("Infisical Response:", JSON.stringify(json)); +``` + + + Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted. + + If an identity access token expires, it can no longer access the Infisical API. A new access token should be obtained by performing another login operation. + diff --git a/docs/images/platform/identities/alicloud/create-access-key.png b/docs/images/platform/identities/alicloud/create-access-key.png new file mode 100644 index 000000000..1297b9b31 Binary files /dev/null and b/docs/images/platform/identities/alicloud/create-access-key.png differ diff --git a/docs/images/platform/identities/alicloud/create-auth-method.png b/docs/images/platform/identities/alicloud/create-auth-method.png new file mode 100644 index 000000000..ec6872f56 Binary files /dev/null and b/docs/images/platform/identities/alicloud/create-auth-method.png differ diff --git a/docs/images/platform/identities/alicloud/credentials.png b/docs/images/platform/identities/alicloud/credentials.png new file mode 100644 index 000000000..0b0f8e0aa Binary files /dev/null and b/docs/images/platform/identities/alicloud/credentials.png differ diff --git a/docs/images/platform/identities/alicloud/user-info.png b/docs/images/platform/identities/alicloud/user-info.png new file mode 100644 index 000000000..4bb18dc7b Binary files /dev/null and b/docs/images/platform/identities/alicloud/user-info.png differ diff --git a/docs/images/platform/identities/alicloud/user-row.png b/docs/images/platform/identities/alicloud/user-row.png new file mode 100644 index 000000000..a16447522 Binary files /dev/null and b/docs/images/platform/identities/alicloud/user-row.png differ diff --git a/docs/images/platform/identities/alicloud/users-page.png b/docs/images/platform/identities/alicloud/users-page.png new file mode 100644 index 000000000..80d2771c3 Binary files /dev/null and b/docs/images/platform/identities/alicloud/users-page.png differ diff --git a/docs/mint.json b/docs/mint.json index bc7f82f79..3382e371e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -329,6 +329,7 @@ { "group": "Machine Identities", "pages": [ + "documentation/platform/identities/alicloud-auth", "documentation/platform/identities/aws-auth", "documentation/platform/identities/azure-auth", "documentation/platform/identities/gcp-auth", @@ -727,6 +728,16 @@ "api-reference/endpoints/gcp-auth/revoke" ] }, + { + "group": "Alibaba Cloud Auth", + "pages": [ + "api-reference/endpoints/alicloud-auth/login", + "api-reference/endpoints/alicloud-auth/attach", + "api-reference/endpoints/alicloud-auth/retrieve", + "api-reference/endpoints/alicloud-auth/update", + "api-reference/endpoints/alicloud-auth/revoke" + ] + }, { "group": "AWS Auth", "pages": [ diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 71f70806a..cc8d0cef6 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -5,6 +5,7 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", [IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth", [IdentityAuthMethod.GCP_AUTH]: "GCP Auth", + [IdentityAuthMethod.ALICLOUD_AUTH]: "Alibaba Cloud Auth", [IdentityAuthMethod.AWS_AUTH]: "AWS Auth", [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth", [IdentityAuthMethod.OCI_AUTH]: "OCI Auth", diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index a9b6eb3e1..de329d393 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -3,6 +3,7 @@ export enum IdentityAuthMethod { UNIVERSAL_AUTH = "universal-auth", KUBERNETES_AUTH = "kubernetes-auth", GCP_AUTH = "gcp-auth", + ALICLOUD_AUTH = "alicloud-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", OCI_AUTH = "oci-auth", diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index a7bd88ce9..52fe52dc2 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { + AddIdentityAliCloudAuthDTO, AddIdentityAwsAuthDTO, AddIdentityAzureAuthDTO, AddIdentityGcpAuthDTO, @@ -21,6 +22,7 @@ import { CreateIdentityUniversalAuthClientSecretRes, CreateTokenIdentityTokenAuthDTO, CreateTokenIdentityTokenAuthRes, + DeleteIdentityAliCloudAuthDTO, DeleteIdentityAwsAuthDTO, DeleteIdentityAzureAuthDTO, DeleteIdentityDTO, @@ -35,6 +37,7 @@ import { DeleteIdentityUniversalAuthDTO, Identity, IdentityAccessToken, + IdentityAliCloudAuth, IdentityAwsAuth, IdentityAzureAuth, IdentityGcpAuth, @@ -47,6 +50,7 @@ import { IdentityUniversalAuth, RevokeTokenDTO, RevokeTokenRes, + UpdateIdentityAliCloudAuthDTO, UpdateIdentityAwsAuthDTO, UpdateIdentityAzureAuthDTO, UpdateIdentityDTO, @@ -553,6 +557,103 @@ export const useDeleteIdentityOciAuth = () => { }); }; +export const useAddIdentityAliCloudAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityAliCloudAuth } + } = await apiRequest.post<{ identityAliCloudAuth: IdentityAliCloudAuth }>( + `/api/v1/auth/alicloud-auth/identities/${identityId}`, + { + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityAliCloudAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) + }); + } + }); +}; + +export const useUpdateIdentityAliCloudAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityAliCloudAuth } + } = await apiRequest.patch<{ identityAliCloudAuth: IdentityAliCloudAuth }>( + `/api/v1/auth/alicloud-auth/identities/${identityId}`, + { + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityAliCloudAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) + }); + } + }); +}; + +export const useDeleteIdentityAliCloudAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityAliCloudAuth } + } = await apiRequest.delete(`/api/v1/auth/alicloud-auth/identities/${identityId}`); + return identityAliCloudAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId) + }); + } + }); +}; + export const useUpdateIdentityOidcAuth = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index adc18ed6f..806c1f0a4 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -6,6 +6,7 @@ import { TReactQueryOptions } from "@app/types/reactQuery"; import { ClientSecretData, IdentityAccessToken, + IdentityAliCloudAuth, IdentityAwsAuth, IdentityAzureAuth, IdentityGcpAuth, @@ -33,6 +34,8 @@ export const identitiesKeys = { getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const, getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const, getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const, + getIdentityAliCloudAuth: (identityId: string) => + [{ identityId }, "identity-alicloud-auth"] as const, getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const, getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const, getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const, @@ -193,6 +196,27 @@ export const useGetIdentityOciAuth = ( }); }; +export const useGetIdentityAliCloudAuth = ( + identityId: string, + options?: TReactQueryOptions["options"] +) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityAliCloudAuth(identityId), + queryFn: async () => { + const { + data: { identityAliCloudAuth } + } = await apiRequest.get<{ identityAliCloudAuth: IdentityAliCloudAuth }>( + `/api/v1/auth/alicloud-auth/identities/${identityId}` + ); + return identityAliCloudAuth; + }, + staleTime: 0, + gcTime: 0, + ...options, + enabled: Boolean(identityId) && (options?.enabled ?? true) + }); +}; + export const useGetIdentityAzureAuth = ( identityId: string, options?: TReactQueryOptions["options"] diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 6b00eb40c..116030131 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -293,6 +293,45 @@ export type DeleteIdentityAwsAuthDTO = { identityId: string; }; +export type IdentityAliCloudAuth = { + identityId: string; + type: "iam"; + allowedArns: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityAliCloudAuthDTO = { + organizationId: string; + identityId: string; + allowedArns: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityAliCloudAuthDTO = { + organizationId: string; + identityId: string; + allowedArns: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type DeleteIdentityAliCloudAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityOciAuth = { identityId: string; type: "iam"; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx new file mode 100644 index 000000000..2d0cc1715 --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm.tsx @@ -0,0 +1,349 @@ +import { useEffect, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Tab, + TabList, + TabPanel, + Tabs +} from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityAliCloudAuth, + useGetIdentityAliCloudAuth, + useUpdateIdentityAliCloudAuth +} from "@app/hooks/api"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityFormTab } from "./types"; + +const schema = z + .object({ + allowedArns: z.string().min(1, "Required"), + accessTokenTTL: z + .string() + .refine( + (value) => Number(value) <= 315360000, + "Access Token TTL cannot be greater than 315360000" + ), + accessTokenMaxTTL: z + .string() + .refine( + (value) => Number(value) <= 315360000, + "Access Token Max TTL cannot be greater than 315360000" + ), + accessTokenNumUsesLimit: z.string(), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().max(50) + }) + .array() + .min(1) + }) + .required(); + +export type FormData = z.infer; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityId?: string; + isUpdate?: boolean; +}; + +export const IdentityAliCloudAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityId, + isUpdate +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityAliCloudAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityAliCloudAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); + + const { data } = useGetIdentityAliCloudAuth(identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + allowedArns: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + allowedArns: data.allowedArns || "", + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + allowedArns: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + allowedArns, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityId) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + allowedArns, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + allowedArns, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); + } catch { + createNotification({ + text: `Failed to ${isUpdate ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
{ + setTabValue( + ["accessTokenTrustedIps"].includes(Object.keys(fields)[0]) + ? IdentityFormTab.Advanced + : IdentityFormTab.Configuration + ); + })} + > + setTabValue(value as IdentityFormTab)}> + + Configuration + Advanced + + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+
+
+ + + +
+
+ ); +}; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx index 8f619029d..0d44bfe64 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModalContent.tsx @@ -8,6 +8,7 @@ import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityAliCloudAuthForm } from "./IdentityAliCloudAuthForm"; import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm"; import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; @@ -45,6 +46,7 @@ const identityAuthMethods = [ { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, { label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH }, { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH }, + { label: "Alibaba Cloud Auth", value: IdentityAuthMethod.ALICLOUD_AUTH }, { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, { label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH }, @@ -172,6 +174,16 @@ export const IdentityAuthMethodModalContent = ({ ) }, + [IdentityAuthMethod.ALICLOUD_AUTH]: { + render: () => ( + + ) + }, + [IdentityAuthMethod.AWS_AUTH]: { render: () => ( { + const { data, isPending } = useGetIdentityAliCloudAuth(identityId); + + if (isPending) { + return ( +
+ +
+ ); + } + + if (!data) { + return ( + + ); + } + + if (popUp.identityAuthMethod.isOpen) { + return ( + + ); + } + + return ( + handlePopUpOpen("identityAuthMethod")} + onDelete={onDelete} + > + + {data.accessTokenTTL} + + + {data.accessTokenMaxTTL} + + + {data.accessTokenNumUsesLimit} + + + {data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")} + + + {data.allowedArns + ?.split(",") + .map((u) => u.trim()) + .join(", ")} + + + ); +}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx index 8a9ee8ea6..8bfbd09b3 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx @@ -6,6 +6,7 @@ import { usePopUp } from "@app/hooks"; import { IdentityAuthMethod, identityAuthToNameMap, + useDeleteIdentityAliCloudAuth, useDeleteIdentityAwsAuth, useDeleteIdentityAzureAuth, useDeleteIdentityGcpAuth, @@ -19,6 +20,7 @@ import { } from "@app/hooks/api"; import { ViewAuthMethodProps } from "./types"; +import { ViewIdentityAliCloudAuthContent } from "./ViewIdentityAliCloudAuthContent"; import { ViewIdentityAwsAuthContent } from "./ViewIdentityAwsAuthContent"; import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent"; import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent"; @@ -63,6 +65,7 @@ export const Content = ({ const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); + const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth(); const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth(); const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); @@ -102,6 +105,10 @@ export const Content = ({ revokeMethod = revokeOciAuth; Component = ViewIdentityOciAuthContent; break; + case IdentityAuthMethod.ALICLOUD_AUTH: + revokeMethod = revokeAliCloudAuth; + Component = ViewIdentityAliCloudAuthContent; + break; case IdentityAuthMethod.OIDC_AUTH: revokeMethod = revokeOidcAuth; Component = ViewIdentityOidcAuthContent;