diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8e70bdae4..b098368c4 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -68,6 +68,7 @@ import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/ import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; +import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; @@ -209,6 +210,7 @@ declare module "fastify" { identityGcpAuth: TIdentityGcpAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory; + identityOciAuth: TIdentityOciAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory; identityJwtAuth: TIdentityJwtAuthServiceFactory; identityLdapAuth: TIdentityLdapAuthServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 4f136bef0..276669b2a 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -119,6 +119,9 @@ import { TIdentityMetadata, TIdentityMetadataInsert, TIdentityMetadataUpdate, + TIdentityOciAuths, + TIdentityOciAuthsInsert, + TIdentityOciAuthsUpdate, TIdentityOidcAuths, TIdentityOidcAuthsInsert, TIdentityOidcAuthsUpdate, @@ -738,6 +741,11 @@ declare module "knex/types/tables" { TIdentityAzureAuthsInsert, TIdentityAzureAuthsUpdate >; + [TableName.IdentityOciAuth]: KnexOriginal.CompositeTableType< + TIdentityOciAuths, + TIdentityOciAuthsInsert, + TIdentityOciAuthsUpdate + >; [TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType< TIdentityOidcAuths, TIdentityOidcAuthsInsert, diff --git a/backend/src/db/migrations/20250508210717_identity-oci-auth.ts b/backend/src/db/migrations/20250508210717_identity-oci-auth.ts new file mode 100644 index 000000000..9512807d1 --- /dev/null +++ b/backend/src/db/migrations/20250508210717_identity-oci-auth.ts @@ -0,0 +1,30 @@ +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.IdentityOciAuth))) { + await knex.schema.createTable(TableName.IdentityOciAuth, (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("tenancyOcid").notNullable(); + t.string("allowedUsernames").nullable(); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityOciAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityOciAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityOciAuth); +} diff --git a/backend/src/db/schemas/identity-oci-auths.ts b/backend/src/db/schemas/identity-oci-auths.ts new file mode 100644 index 000000000..e0be86b78 --- /dev/null +++ b/backend/src/db/schemas/identity-oci-auths.ts @@ -0,0 +1,26 @@ +// 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 IdentityOciAuthsSchema = 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(), + tenancyOcid: z.string(), + allowedUsernames: z.string().nullable().optional() +}); + +export type TIdentityOciAuths = z.infer; +export type TIdentityOciAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityOciAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index ebbe417c4..0bf44c413 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -37,6 +37,7 @@ export * from "./identity-gcp-auths"; export * from "./identity-jwt-auths"; export * from "./identity-kubernetes-auths"; export * from "./identity-metadata"; +export * from "./identity-oci-auths"; export * from "./identity-oidc-auths"; export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 912c8ac46..730474ad1 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -79,6 +79,7 @@ export enum TableName { IdentityAzureAuth = "identity_azure_auths", IdentityUaClientSecret = "identity_ua_client_secrets", IdentityAwsAuth = "identity_aws_auths", + IdentityOciAuth = "identity_oci_auths", IdentityOidcAuth = "identity_oidc_auths", IdentityJwtAuth = "identity_jwt_auths", IdentityLdapAuth = "identity_ldap_auths", @@ -233,6 +234,7 @@ export enum IdentityAuthMethod { GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", + OCI_AUTH = "oci-auth", OIDC_AUTH = "oidc-auth", JWT_AUTH = "jwt-auth", LDAP_AUTH = "ldap-auth" diff --git a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts index e44693643..26e8cad3b 100644 --- a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts +++ b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts @@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds) }) .refine((data) => ms(data.maxTTL) >= ms(data.ttl), { - message: "Max TLL must be greater than or equal to TTL", + message: "Max TTL must be greater than or equal to TTL", path: ["maxTTL"] }), response: { 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 03f11219e..365ada987 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -162,6 +162,12 @@ export enum EventType { REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth", GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth", + LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth", + ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth", + UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth", + REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth", + GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth", + LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth", ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth", UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth", @@ -1009,6 +1015,55 @@ interface GetIdentityAwsAuthEvent { }; } +interface LoginIdentityOciAuthEvent { + type: EventType.LOGIN_IDENTITY_OCI_AUTH; + metadata: { + identityId: string; + identityOciAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityOciAuthEvent { + type: EventType.ADD_IDENTITY_OCI_AUTH; + metadata: { + identityId: string; + tenancyOcid: string; + allowedUsernames: string | null; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface DeleteIdentityOciAuthEvent { + type: EventType.REVOKE_IDENTITY_OCI_AUTH; + metadata: { + identityId: string; + }; +} + +interface UpdateIdentityOciAuthEvent { + type: EventType.UPDATE_IDENTITY_OCI_AUTH; + metadata: { + identityId: string; + tenancyOcid?: string; + allowedUsernames: string | null; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityOciAuthEvent { + type: EventType.GET_IDENTITY_OCI_AUTH; + metadata: { + identityId: string; + }; +} + interface LoginIdentityAzureAuthEvent { type: EventType.LOGIN_IDENTITY_AZURE_AUTH; metadata: { @@ -2914,6 +2969,11 @@ export type Event = | UpdateIdentityAwsAuthEvent | GetIdentityAwsAuthEvent | DeleteIdentityAwsAuthEvent + | LoginIdentityOciAuthEvent + | AddIdentityOciAuthEvent + | UpdateIdentityOciAuthEvent + | GetIdentityOciAuthEvent + | DeleteIdentityOciAuthEvent | LoginIdentityAzureAuthEvent | AddIdentityAzureAuthEvent | DeleteIdentityAzureAuthEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a4556a75d..cabc8e1db 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -14,6 +14,7 @@ export enum ApiDocsTags { UniversalAuth = "Universal Auth", GcpAuth = "GCP Auth", AwsAuth = "AWS Auth", + OciAuth = "OCI Auth", AzureAuth = "Azure Auth", KubernetesAuth = "Kubernetes Auth", JwtAuth = "JWT Auth", @@ -271,6 +272,40 @@ export const AWS_AUTH = { } } as const; +export const OCI_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login.", + userOcid: "The OCID of the user attempting login.", + headers: "The headers of the signed request." + }, + ATTACH: { + identityId: "The ID of the identity to attach the configuration onto.", + tenancyOcid: "The OCID of your tenancy.", + allowedUsernames: + "The comma-separated list of trusted OCI account usernames 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.", + tenancyOcid: "The OCID of your tenancy.", + allowedUsernames: + "The comma-separated list of trusted OCI account usernames 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 AZURE_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 844f6c31e..b05b253d6 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -162,6 +162,8 @@ import { identityKubernetesAuthDALFactory } from "@app/services/identity-kuberne import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal"; import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; +import { identityOciAuthDALFactory } from "@app/services/identity-oci-auth/identity-oci-auth-dal"; +import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service"; import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -355,6 +357,7 @@ export const registerRoutes = async ( const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db); + const identityOciAuthDAL = identityOciAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db); const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db); @@ -1451,6 +1454,14 @@ export const registerRoutes = async ( licenseService }); + const identityOciAuthService = identityOciAuthServiceFactory({ + identityAccessTokenDAL, + identityOciAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService + }); + const identityOidcAuthService = identityOidcAuthServiceFactory({ identityOidcAuthDAL, identityOrgMembershipDAL, @@ -1737,6 +1748,7 @@ export const registerRoutes = async ( identityGcpAuth: identityGcpAuthService, identityAwsAuth: identityAwsAuthService, identityAzureAuth: identityAzureAuthService, + identityOciAuth: identityOciAuthService, identityOidcAuth: identityOidcAuthService, identityJwtAuth: identityJwtAuthService, identityLdapAuth: identityLdapAuthService, diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts new file mode 100644 index 000000000..de9866c85 --- /dev/null +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -0,0 +1,338 @@ +import { z } from "zod"; + +import { IdentityOciAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { 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 { validateTenancy, validateUsernames } from "@app/services/identity-oci-auth/identity-oci-auth-validators"; +import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns"; + +export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/oci-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.OciAuth], + description: "Login with OCI Auth", + body: z.object({ + identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId), + userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid), + headers: z + .object({ + authorization: z.string(), + host: z.string(), + "x-date": z.string() + }) + .describe(OCI_AUTH.LOGIN.headers) + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityOciAuth.login(req.body); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_OCI_AUTH, + metadata: { + identityId: identityOciAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityOciAuthId: identityOciAuth.id + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityOciAuth.accessTokenTTL, + accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/oci-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.OciAuth], + description: "Attach OCI Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim().describe(OCI_AUTH.ATTACH.identityId) + }), + body: z + .object({ + tenancyOcid: validateTenancy.describe(OCI_AUTH.ATTACH.tenancyOcid), + allowedUsernames: validateUsernames.describe(OCI_AUTH.ATTACH.allowedUsernames), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(OCI_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(OCI_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(OCI_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OCI_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + response: { + 200: z.object({ + identityOciAuth: IdentityOciAuthsSchema + }) + } + }, + handler: async (req) => { + const identityOciAuth = await server.services.identityOciAuth.attachOciAuth({ + 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: identityOciAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_OCI_AUTH, + metadata: { + identityId: identityOciAuth.identityId, + tenancyOcid: identityOciAuth.tenancyOcid, + allowedUsernames: identityOciAuth.allowedUsernames || null, + accessTokenTTL: identityOciAuth.accessTokenTTL, + accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityOciAuth }; + } + }); + + server.route({ + method: "PATCH", + url: "/oci-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.OciAuth], + description: "Update OCI Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(OCI_AUTH.UPDATE.identityId) + }), + body: z + .object({ + tenancyOcid: validateTenancy.describe(OCI_AUTH.UPDATE.tenancyOcid), + allowedUsernames: validateUsernames.describe(OCI_AUTH.UPDATE.allowedUsernames), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional() + .describe(OCI_AUTH.UPDATE.accessTokenTrustedIps), + accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(OCI_AUTH.UPDATE.accessTokenTTL), + accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(OCI_AUTH.UPDATE.accessTokenNumUsesLimit), + accessTokenMaxTTL: z + .number() + .int() + .max(315360000) + .min(0) + .optional() + .describe(OCI_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({ + identityOciAuth: IdentityOciAuthsSchema + }) + } + }, + handler: async (req) => { + const identityOciAuth = await server.services.identityOciAuth.updateOciAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId, + allowedUsernames: req.body.allowedUsernames || null + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityOciAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_OCI_AUTH, + metadata: { + identityId: identityOciAuth.identityId, + tenancyOcid: identityOciAuth.tenancyOcid, + allowedUsernames: identityOciAuth.allowedUsernames || null, + accessTokenTTL: identityOciAuth.accessTokenTTL, + accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityOciAuth }; + } + }); + + server.route({ + method: "GET", + url: "/oci-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.OciAuth], + description: "Retrieve OCI Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(OCI_AUTH.RETRIEVE.identityId) + }), + response: { + 200: z.object({ + identityOciAuth: IdentityOciAuthsSchema + }) + } + }, + handler: async (req) => { + const identityOciAuth = await server.services.identityOciAuth.getOciAuth({ + 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: identityOciAuth.orgId, + event: { + type: EventType.GET_IDENTITY_OCI_AUTH, + metadata: { + identityId: identityOciAuth.identityId + } + } + }); + return { identityOciAuth }; + } + }); + + server.route({ + method: "DELETE", + url: "/oci-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.OciAuth], + description: "Delete OCI Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().describe(OCI_AUTH.REVOKE.identityId) + }), + response: { + 200: z.object({ + identityOciAuth: IdentityOciAuthsSchema + }) + } + }, + handler: async (req) => { + const identityOciAuth = await server.services.identityOciAuth.revokeIdentityOciAuth({ + 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: identityOciAuth.orgId, + event: { + type: EventType.REVOKE_IDENTITY_OCI_AUTH, + metadata: { + identityId: identityOciAuth.identityId + } + } + }); + + return { identityOciAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 7950b9efe..018e457fa 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -20,6 +20,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router"; import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; +import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; @@ -63,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter); + await authRouter.register(registerIdentityOciAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter); await authRouter.register(registerIdentityJwtAuthRouter); await authRouter.register(registerIdentityLdapAuthRouter); 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 a2a067cad..fea12d3ee 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 @@ -36,6 +36,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { `${TableName.Identity}.id`, `${TableName.IdentityKubernetesAuth}.identityId` ) + .leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`) .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) @@ -46,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), @@ -63,6 +65,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws, trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, + trustedIpsOciAuth: doc.accessTokenTrustedIpsOci, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt, 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 cd79981fe..6a082c432 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 @@ -182,6 +182,7 @@ export const identityAccessTokenServiceFactory = ({ [IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth, [IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth, [IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth, + [IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth, [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-dal.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-dal.ts new file mode 100644 index 000000000..95278c75a --- /dev/null +++ b/backend/src/services/identity-oci-auth/identity-oci-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 TIdentityOciAuthDALFactory = ReturnType; + +export const identityOciAuthDALFactory = (db: TDbClient) => { + return ormify(db, TableName.IdentityOciAuth); +}; diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts new file mode 100644 index 000000000..f3df97330 --- /dev/null +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -0,0 +1,371 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ForbiddenError } from "@casl/ability"; +import { AxiosError } from "axios"; +import jwt from "jsonwebtoken"; +import RE2 from "re2"; + +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 { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +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 { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal"; +import { + TAttachOciAuthDTO, + TGetOciAuthDTO, + TLoginOciAuthDTO, + TOciGetUserResponse, + TRevokeOciAuthDTO, + TUpdateOciAuthDTO +} from "./identity-oci-auth-types"; + +type TIdentityOciAuthServiceFactoryDep = { + identityAccessTokenDAL: Pick; + identityOciAuthDAL: Pick; + identityOrgMembershipDAL: Pick; + licenseService: Pick; + permissionService: Pick; +}; + +export type TIdentityOciAuthServiceFactory = ReturnType; + +export const identityOciAuthServiceFactory = ({ + identityAccessTokenDAL, + identityOciAuthDAL, + identityOrgMembershipDAL, + licenseService, + permissionService +}: TIdentityOciAuthServiceFactoryDep) => { + const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { + const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); + if (!identityOciAuth) { + throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); + } + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId }); + + await blockLocalAndPrivateIpAddresses(headers.host); + + // Validate OCI host format + if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { + throw new BadRequestError({ + message: "Invalid OCI host format. Expected format: identity..oraclecloud.com" + }); + } + + const { data } = await request + .get(`https://${headers.host}/20160918/users/${userOcid}`, { + headers + }) + .catch((err: AxiosError) => { + logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud"); + throw err; + }); + + if (data.compartmentId !== identityOciAuth.tenancyOcid) { + throw new UnauthorizedError({ + message: "Access denied: OCI account isn't part of tenancy." + }); + } + + if (identityOciAuth.allowedUsernames) { + const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name); + + if (!isAccountAllowed) + throw new UnauthorizedError({ + message: "Access denied: OCI account username not allowed." + }); + } + + // Generate the token + const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityOciAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityOciAuth.accessTokenTTL, + accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.OCI_AUTH + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityOciAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + return { + identityOciAuth, + accessToken, + identityAccessToken, + identityMembershipOrg + }; + }; + + const attachOciAuth = async ({ + identityId, + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId, + isActorSuperAdmin + }: TAttachOciAuthDTO) => { + 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.OCI_AUTH)) { + throw new BadRequestError({ + message: "Failed to add OCI 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 identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => { + const doc = await identityOciAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + type: "iam", + tenancyOcid, + allowedUsernames, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + return doc; + }); + return { ...identityOciAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateOciAuth = async ({ + identityId, + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateOciAuthDTO) => { + 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.OCI_AUTH)) { + throw new NotFoundError({ + message: "The identity does not have OCI Auth attached" + }); + } + + const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityOciAuth.accessTokenTTL) > (accessTokenMaxTTL || identityOciAuth.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 updatedOciAuth = await identityOciAuthDAL.updateById(identityOciAuth.id, { + tenancyOcid, + allowedUsernames, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId }; + }; + + const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => { + 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.OCI_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have OCI Auth attached" + }); + } + + const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId }; + }; + + const revokeIdentityOciAuth = async ({ + identityId, + actorId, + actor, + actorAuthMethod, + actorOrgId + }: TRevokeOciAuthDTO) => { + 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.OCI_AUTH)) { + throw new BadRequestError({ + message: "The identity does not have OCI 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 OCI auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const revokedIdentityOciAuth = await identityOciAuthDAL.transaction(async (tx) => { + const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx); + await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx); + + return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId }; + }); + return revokedIdentityOciAuth; + }; + + return { + login, + attachOciAuth, + updateOciAuth, + getOciAuth, + revokeIdentityOciAuth + }; +}; diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts new file mode 100644 index 000000000..c7a131bde --- /dev/null +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts @@ -0,0 +1,53 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TLoginOciAuthDTO = { + identityId: string; + userOcid: string; + headers: { + authorization: string; + host: string; + "x-date": string; + }; +}; + +export type TAttachOciAuthDTO = { + identityId: string; + tenancyOcid: string; + allowedUsernames: string | null; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; + isActorSuperAdmin?: boolean; +} & Omit; + +export type TUpdateOciAuthDTO = { + identityId: string; + tenancyOcid: string; + allowedUsernames: string | null; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetOciAuthDTO = { + identityId: string; +} & Omit; + +export type TRevokeOciAuthDTO = { + identityId: string; +} & Omit; + +export type TOciGetUserResponse = { + email: string; + emailVerified: boolean; + timeModified: string; + isMfaActivated: boolean; + id: string; + compartmentId: string; + name: string; + timeCreated: string; + freeformTags: { [key: string]: string }; + lifecycleState: string; +}; diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-validators.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-validators.ts new file mode 100644 index 000000000..49100b46c --- /dev/null +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-validators.ts @@ -0,0 +1,32 @@ +import RE2 from "re2"; +import { z } from "zod"; + +const usernameSchema = z + .string() + .min(1, "Username cannot be empty") + .refine((val) => new RE2("^[a-zA-Z0-9._@-]+$").test(val), "Invalid OCI username format"); +export const validateUsernames = z + .string() + .trim() + .max(500, "Input exceeds the maximum limit of 500 characters") + .nullish() + .transform((val) => { + if (!val) return []; + return val + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + }) + .refine((arr) => arr.every((name) => usernameSchema.safeParse(name).success), { + message: "One or more usernames are invalid" + }) + .transform((arr) => (arr.length > 0 ? arr.join(", ") : null)); + +export const validateTenancy = z + .string() + .trim() + .min(1, "Tenancy OCID cannot be empty.") + .refine( + (val) => new RE2("^ocid1\\.tenancy\\.oc1\\..+$").test(val), + "Invalid Tenancy OCID format. Must start with ocid1.tenancy.oc1." + ); diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index bc4f4a303..3c8bc5d37 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -8,6 +8,7 @@ import { TIdentityAzureAuths, TIdentityGcpAuths, TIdentityKubernetesAuths, + TIdentityOciAuths, TIdentityOidcAuths, TIdentityTokenAuths, TIdentityUniversalAuths @@ -66,6 +67,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) + .leftJoin( + TableName.IdentityOciAuth, + `${TableName.IdentityProjectMembership}.identityId`, + `${TableName.IdentityOciAuth}.identityId` + ) .leftJoin( TableName.IdentityOidcAuth, `${TableName.IdentityProjectMembership}.identityId`, @@ -107,6 +113,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) @@ -270,6 +277,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { `${TableName.Identity}.id`, `${TableName.IdentityKubernetesAuth}.identityId` ) + .leftJoin( + TableName.IdentityOciAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityOciAuth}.identityId` + ) .leftJoin( TableName.IdentityOidcAuth, `${TableName.Identity}.id`, @@ -309,6 +321,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) @@ -336,6 +349,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { awsId, gcpId, kubernetesId, + ociId, oidcId, azureId, tokenId, @@ -356,6 +370,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { awsId, gcpId, kubernetesId, + ociId, oidcId, azureId, tokenId diff --git a/backend/src/services/identity/identity-fns.ts b/backend/src/services/identity/identity-fns.ts index 6c77618e4..3fa2482aa 100644 --- a/backend/src/services/identity/identity-fns.ts +++ b/backend/src/services/identity/identity-fns.ts @@ -5,6 +5,7 @@ export const buildAuthMethods = ({ gcpId, awsId, kubernetesId, + ociId, oidcId, azureId, tokenId, @@ -15,6 +16,7 @@ export const buildAuthMethods = ({ gcpId?: string; awsId?: string; kubernetesId?: string; + ociId?: string; oidcId?: string; azureId?: string; tokenId?: string; @@ -26,6 +28,7 @@ export const buildAuthMethods = ({ ...[gcpId ? IdentityAuthMethod.GCP_AUTH : null], ...[awsId ? IdentityAuthMethod.AWS_AUTH : null], ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], + ...[ociId ? IdentityAuthMethod.OCI_AUTH : null], ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 8b5032945..af5537249 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -8,6 +8,7 @@ import { TIdentityGcpAuths, TIdentityJwtAuths, TIdentityKubernetesAuths, + TIdentityOciAuths, TIdentityOidcAuths, TIdentityOrgMemberships, TIdentityTokenAuths, @@ -62,6 +63,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) + .leftJoin( + TableName.IdentityOciAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOciAuth}.identityId` + ) .leftJoin( TableName.IdentityOidcAuth, `${TableName.IdentityOrgMembership}.identityId`, @@ -95,6 +101,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), @@ -186,6 +193,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { "paginatedIdentity.identityId", `${TableName.IdentityKubernetesAuth}.identityId` ) + .leftJoin( + TableName.IdentityOciAuth, + "paginatedIdentity.identityId", + `${TableName.IdentityOciAuth}.identityId` + ) .leftJoin( TableName.IdentityOidcAuth, "paginatedIdentity.identityId", @@ -226,6 +238,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), @@ -269,6 +282,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { gcpId, jwtId, kubernetesId, + ociId, oidcId, azureId, tokenId, @@ -301,6 +315,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { awsId, gcpId, kubernetesId, + ociId, oidcId, azureId, tokenId, @@ -401,6 +416,11 @@ export const identityOrgDALFactory = (db: TDbClient) => { `${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityKubernetesAuth}.identityId` ) + .leftJoin( + TableName.IdentityOciAuth, + `${TableName.IdentityOrgMembership}.identityId`, + `${TableName.IdentityOciAuth}.identityId` + ) .leftJoin( TableName.IdentityOidcAuth, `${TableName.IdentityOrgMembership}.identityId`, @@ -441,6 +461,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), @@ -485,6 +506,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { gcpId, jwtId, kubernetesId, + ociId, oidcId, azureId, tokenId, @@ -517,6 +539,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { awsId, gcpId, kubernetesId, + ociId, oidcId, azureId, tokenId, diff --git a/docs/api-reference/endpoints/oci-auth/attach.mdx b/docs/api-reference/endpoints/oci-auth/attach.mdx new file mode 100644 index 000000000..039e99064 --- /dev/null +++ b/docs/api-reference/endpoints/oci-auth/attach.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach" +openapi: "POST /api/v1/auth/oci-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oci-auth/login.mdx b/docs/api-reference/endpoints/oci-auth/login.mdx new file mode 100644 index 000000000..400addcbd --- /dev/null +++ b/docs/api-reference/endpoints/oci-auth/login.mdx @@ -0,0 +1,4 @@ +--- +title: "Login" +openapi: "POST /api/v1/auth/oci-auth/login" +--- diff --git a/docs/api-reference/endpoints/oci-auth/retrieve.mdx b/docs/api-reference/endpoints/oci-auth/retrieve.mdx new file mode 100644 index 000000000..31883fb77 --- /dev/null +++ b/docs/api-reference/endpoints/oci-auth/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve" +openapi: "GET /api/v1/auth/oci-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oci-auth/revoke.mdx b/docs/api-reference/endpoints/oci-auth/revoke.mdx new file mode 100644 index 000000000..5cc609003 --- /dev/null +++ b/docs/api-reference/endpoints/oci-auth/revoke.mdx @@ -0,0 +1,4 @@ +--- +title: "Revoke" +openapi: "DELETE /api/v1/auth/oci-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/endpoints/oci-auth/update.mdx b/docs/api-reference/endpoints/oci-auth/update.mdx new file mode 100644 index 000000000..72c1dfdf0 --- /dev/null +++ b/docs/api-reference/endpoints/oci-auth/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/auth/oci-auth/identities/{identityId}" +--- diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index bdd7df83b..4358e18d2 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -13,17 +13,17 @@ To interact with the Infisical API, you will need to obtain an access token. Fol There are a few reasons for why this might happen: - + - You have insufficient organization permissions to create, read, update, delete identities. - The identity you are trying to read, update, or delete is more privileged than yourself. - The role you are trying to create an identity for or update an identity to is more privileged than yours. There are a few reasons for why this might happen: - + - The client secret or access token has expired. - - The identity is insufficently permissioned to interact with the resources you wish to access. + - The identity is insufficiently permissioned to interact with the resources you wish to access. - You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE. - The client secret/access token is being used from an untrusted IP. - \ No newline at end of file + diff --git a/docs/documentation/getting-started/api.mdx b/docs/documentation/getting-started/api.mdx index 48a6f2ee0..c638c4d70 100644 --- a/docs/documentation/getting-started/api.mdx +++ b/docs/documentation/getting-started/api.mdx @@ -10,15 +10,15 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis To create a project, head to your Organization Overview and press **Add New Project**; we'll call the project **Demo App**. ![create project](../../images/getting-started/api/org-create-project-1.png) - + ![create project](../../images/getting-started/api/org-create-project-2.png) - + Next, let's head to the **Development** environment of the project and add a secret `FOO=BAR` to it. - + ![explore project env](../../images/getting-started/api/project-explore-env.png) - + ![create secret](../../images/getting-started/api/project-create-secret.png) - + ![project dashboard](../../images/getting-started/api/project-dashboard.png) @@ -29,13 +29,13 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis Next, we need to create an identity to represent your application. To create one, head to your Organization Settings > Access Control > Machine 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. - + ![identities organization create](../../images/platform/identities/identities-org-create.png) - + Once you've created an identity, you'll be prompted to configure the **Universal Auth** authentication method for it. - + ![identities organization create auth method](../../images/platform/identities/identities-org-create-auth-method.png) @@ -44,7 +44,7 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis of the identity and a **Client Secret** for it; you can think of these credentials akin to a username and password used to authenticate with the Infisical API. With that, press on the key icon on the identity to generate a **Client Secret** for it. - + ![identities client secret create](../../images/platform/identities/identities-org-client-secret.png) ![identities client secret create](../../images/platform/identities/identities-org-client-secret-create-1.png) ![identities client secret create](../../images/platform/identities/identities-org-client-secret-create-2.png) @@ -55,14 +55,14 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis Next, select the identity you want to add to the project and the role you want to assign it. ![identities project](../../images/platform/identities/identities-project.png) - + ![identities project create](../../images/platform/identities/identities-project-create.png) To access the Infisical API as the identity, you should first perform a login operation that is to exchange the **Client ID** and **Client Secret** of the identity for an access token by making a request to the `/api/v1/auth/universal-auth/login` endpoint. - + #### Sample request ``` @@ -71,9 +71,9 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis --data-urlencode 'clientSecret=' \ --data-urlencode 'clientId=' ``` - + #### Sample response - + ``` { "accessToken": "...", @@ -83,9 +83,9 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis ``` Next, we can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) to read/write secrets - + - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, @@ -96,12 +96,12 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis Finally, you can fetch the secret `FOO=BAR` back from **Step 1** by including the access token in the previous step in another request to the `/api/v3/secrets/raw/{secretName}` endpoint. ### Sample request - + ``` curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/FOO?workspaceId=657830d579cfc8415d06ce5b&environment=dev' \ --header 'Authorization: Bearer ' ``` - + ### Sample response ``` @@ -118,11 +118,11 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis } } ``` - + Note that you can fetch a list of secrets back by making a request to the `/api/v3/secrets/raw` endpoint. See also: -- [API Reference](/api-reference/overview/introduction) \ No newline at end of file +- [API Reference](/api-reference/overview/introduction) diff --git a/docs/documentation/platform/identities/aws-auth.mdx b/docs/documentation/platform/identities/aws-auth.mdx index 1c853957b..f27d5c7bf 100644 --- a/docs/documentation/platform/identities/aws-auth.mdx +++ b/docs/documentation/platform/identities/aws-auth.mdx @@ -311,7 +311,7 @@ access the Infisical API using the AWS Auth authentication method. - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, diff --git a/docs/documentation/platform/identities/azure-auth.mdx b/docs/documentation/platform/identities/azure-auth.mdx index 9576c4d0f..7a7c112ef 100644 --- a/docs/documentation/platform/identities/azure-auth.mdx +++ b/docs/documentation/platform/identities/azure-auth.mdx @@ -173,7 +173,7 @@ access the Infisical API using the Azure Auth authentication method. We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using Azure Auth as they handle the authentication process including retrieving the client access token. - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation. diff --git a/docs/documentation/platform/identities/gcp-auth.mdx b/docs/documentation/platform/identities/gcp-auth.mdx index 17dc5acd9..8d6a1f177 100644 --- a/docs/documentation/platform/identities/gcp-auth.mdx +++ b/docs/documentation/platform/identities/gcp-auth.mdx @@ -168,7 +168,7 @@ access the Infisical API using the GCP ID Token authentication method. We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token. - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation. @@ -179,7 +179,7 @@ access the Infisical API using the GCP ID Token authentication method. - + ## Diagram The following sequence diagram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical. @@ -352,7 +352,7 @@ access the Infisical API using the GCP IAM authentication method. We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token. - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation. @@ -361,5 +361,5 @@ access the Infisical API using the GCP IAM authentication method. - + diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx index cfa0e861a..9daff1e81 100644 --- a/docs/documentation/platform/identities/kubernetes-auth.mdx +++ b/docs/documentation/platform/identities/kubernetes-auth.mdx @@ -56,7 +56,7 @@ In the following steps, we explore how to create and use identities for your app - + **When to use this option**: Choose this approach when you want centralized authentication management. Only one service account needs special permissions, and your application service accounts remain unchanged. @@ -190,7 +190,7 @@ In the following steps, we explore how to create and use identities for your app Here's some more guidance on each field: - Kubernetes Host / Base Kubernetes API URL: The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running `kubectl cluster-info`. - - Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5(Reviewer Tab). If omitted, the client's own JWT will be used instead, which requires the client to have the `system:auth-delegator` ClusterRole binding. + - Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5(Reviewer Tab). If omitted, the client's own JWT will be used instead, which requires the client to have the `system:auth-delegator` ClusterRole binding. This is shown in step 1, option 2. - Allowed Service Account Names: A comma-separated list of trusted service account names that are allowed to authenticate with Infisical. - Allowed Namespaces: A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical. @@ -257,7 +257,7 @@ In the following steps, we explore how to create and use identities for your app - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 exceeds its max ttl, it can no longer authenticate with the Infisical API. In this case, @@ -280,7 +280,7 @@ In the following steps, we explore how to create and use identities for your app There are a few reasons for why this might happen: - The access token has expired. -- The identity is insufficently permissioned to interact with the resources you wish to access. +- The identity is insufficiently permissioned to interact with the resources you wish to access. - The client access token is being used from an untrusted IP. diff --git a/docs/documentation/platform/identities/oci-auth.mdx b/docs/documentation/platform/identities/oci-auth.mdx new file mode 100644 index 000000000..ef5fafa4c --- /dev/null +++ b/docs/documentation/platform/identities/oci-auth.mdx @@ -0,0 +1,212 @@ +--- +title: OCI Auth +description: "Learn how to authenticate with Infisical using OCI user accounts." +--- + +**OCI Auth** is an OCI-native authentication method that verifies Oracle Cloud Infrastructure users through signature validation, allowing secure access to Infisical resources. + +## Diagram + +The following sequence diagram illustrates the OCI Auth workflow for authenticating OCI users with Infisical. + +```mermaid +sequenceDiagram + participant Client + participant Infisical + participant OCI + + 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/oci-auth/login + + Note over Infisical,OCI: Step 3: Request verification + Infisical->>OCI: Forward signed request + OCI-->>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 OCI user by verifying its identity and checking that it meets specific requirements (e.g., its username is authorized, its part of a tenancy) at the `/api/v1/auth/oci-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](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm) a `/20160918/users/{userId}` request using an OCI user's [private key](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#Required_Keys_and_OCIDs); this is done using the [OCI SDK](https://infisical.com/docs/documentation/platform/identities/oci-auth#accessing-the-infisical-api-with-the-identity) or API. +2. The client sends the signed request's headers and their user OCID to Infisical at the `/api/v1/auth/oci-auth/login` endpoint. +3. Infisical reconstructs the request and sends it to OCI via the [Get User](https://docs.oracle.com/en/engineered-systems/private-cloud-appliance/3.0-latest/ceapi/op-20160918-users-user_id-get.html) endpoint for verification and obtains the identity associated with the OCI user. +4. Infisical checks the user's properties against set criteria such as **Allowed Usernames** and **Tenancy OCID**. +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 OCI user with credentials such as the private key. If you're unaware of how to create a user and obtain the needed credentials, expand the menu below. + + + + + ![Search Domains](/images/app-connections/oci/search-domains.png) + + + Select the domain in which you want to create the Infisical user account. + + ![Select Domain](/images/app-connections/oci/select-domain.png) + + + ![Select Users](/images/app-connections/oci/select-users.png) + + + ![Click Create User](/images/app-connections/oci/click-create-user.png) + + + The name, email, and username can be anything. + + ![Create User](/images/app-connections/oci/create-user.png) + + + After you've created a user, you'll be redirected to the user's page. Navigate to 'API keys'. + + ![Select API Keys](/images/app-connections/oci/select-api-keys.png) + + + Click on 'Add API key' and then download or import the private key. After you've obtained the private key, click 'Add'. + + ![Add API Key](/images/app-connections/oci/add-api-key.png) + + + At the end of the downloaded private key file, you'll see `OCI_API_KEY`. This is not apart of the private key, and should not be included when you use the private key to sign requests. + + + + + After creating the API key, you'll be shown a modal with relevant information. Save the highlighted values (and the private key) for later steps. + + ![User Info](/images/app-connections/oci/user-info.png) + + + + +## Guide + +In the following steps, we explore how to create and use identities for your workloads and applications on OCI to +access the Infisical API using the OCI request signing authentication method. + +### 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 OCI 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 OCI Auth Method. + +![identities create oci auth method](/images/platform/identities/identities-org-create-oci-auth-method.png) + +Here's some information about each field: +- **Tenancy OCID:** The OCID of your tenancy. All users authenticating must be part of this Tenancy. +- **Allowed Usernames:** A comma-separated list of trusted OCI users 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 [Get User](https://docs.oracle.com/en/engineered-systems/private-cloud-appliance/3.0-latest/ceapi/op-20160918-users-user_id-get.html) request using [OCI Signature v1](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm#Request_Signatures) and then make a request to the `/api/v1/auth/oci-auth/login` endpoint passing the signed header data and user OCID. + +Below is an example of how you can authenticate with Infisical using the `oci-sdk` for NodeJS. + +```typescript +import { common } from "oci-sdk"; + +// Change these credentials to match your OCI user +const tenancyId = "ocid1.tenancy.oc1..example"; +const userId = "ocid1.user.oc1..example"; +const fingerprint = "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"; +const region = "us-ashburn-1"; +const privateKey = "..."; // Must be PEM format + +const provider = new common.SimpleAuthenticationDetailsProvider( + tenancyId, + userId, + fingerprint, + privateKey, + null, + common.Region.fromRegionId(region), +); + +// Build request +const headers = new Headers({ + host: `identity.${region}.oraclecloud.com`, +}); + +const request: common.HttpRequest = { + method: "GET", + uri: `/20160918/users/${userId}`, + headers, + body: null, +}; + +// Sign request +const signer = new common.DefaultRequestSigner(provider); +await signer.signHttpRequest(request); + +// Forward signed request to Infisical +const requestAsJson = { + identityId: "2dd11664-68e3-471d-b366-907206ab1bff", + userOcid: userId, + headers: Object.fromEntries(request.headers.entries()), +}; + +const res = await fetch("https://app.infisical.com/api/v1/auth/oci-auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestAsJson), +}); + +const json = await res.json(); + +console.log("Infisical Response:", 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/documentation/platform/identities/oidc-auth/circleci.mdx b/docs/documentation/platform/identities/oidc-auth/circleci.mdx index 6849b77f9..bb5999f55 100644 --- a/docs/documentation/platform/identities/oidc-auth/circleci.mdx +++ b/docs/documentation/platform/identities/oidc-auth/circleci.mdx @@ -163,7 +163,7 @@ In the following steps, we explore how to create and use identities to access th } ``` - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx index 9a39adba3..f847f51fe 100644 --- a/docs/documentation/platform/identities/oidc-auth/general.mdx +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -159,7 +159,7 @@ In the following steps, we explore how to create and use identities to access th - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx index a377ac37c..567f38d05 100644 --- a/docs/documentation/platform/identities/oidc-auth/github.mdx +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -159,7 +159,7 @@ In the following steps, we explore how to create and use identities to access th - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 authenticate with the Infisical API. In this case, diff --git a/docs/documentation/platform/identities/token-auth.mdx b/docs/documentation/platform/identities/token-auth.mdx index 500adf509..f31e86517 100644 --- a/docs/documentation/platform/identities/token-auth.mdx +++ b/docs/documentation/platform/identities/token-auth.mdx @@ -77,9 +77,9 @@ using the Token Auth authentication method. - In order to use the identity with Token Auth, you'll need to create an (access) token; you can think of this token akin + In order to use the identity with Token Auth, you'll need to create an (access) token; you can think of this token akin to an API Key used to authenticate with the Infisical API. With that, press **Create Token**. - + ![identities client secret create](/images/platform/identities/identities-token-auth-create-1.png) ![identities client secret create](/images/platform/identities/identities-token-auth-create-2.png) @@ -106,7 +106,7 @@ using the Token Auth authentication method. to authenticate with the [Infisical API](/api-reference/overview/introduction). - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 in the Token Auth configuration. If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, @@ -121,14 +121,14 @@ using the Token Auth authentication method. There are a few reasons for why this might happen: - + - The access token has expired. If this is the case, you should obtain a new access token or consider extending the token's TTL. - - The identity is insufficently permissioned to interact with the resources you wish to access. + - The identity is insufficiently permissioned to interact with the resources you wish to access. - The access token is being used from an untrusted IP. A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires. - + In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. A token can be renewed any number of times where each call to renew it can extend the token's lifetime by increments of the access token's TTL. diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 30f1f10d2..44f468a17 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -84,15 +84,15 @@ using the Universal Auth authentication method. In order to use the identity, you'll need the non-sensitive **Client ID** of the identity and a **Client Secret** for it; you can think of these credentials akin to a username - and password used to authenticate with the Infisical API. + and password used to authenticate with the Infisical API. With that, press **Create Client Secret**. - + ![identities client secret create](/images/platform/identities/identities-universal-auth-create-1.png) ![identities client secret create](/images/platform/identities/identities-universal-auth-create-2.png) ![identities client secret create](/images/platform/identities/identities-universal-auth-create-3.png) - + Feel free to input any (optional) details for the **Client Secret** configuration: - + - Description: A description for the **Client Secret**. - TTL (default is `0`): The time-to-live for the **Client Secret**. By default, the TTL will be set to 0 which implies that the **Client Secret** will never expire; a value of `0` implies an infinite lifetime. - Max Number of Uses (default is `0`): The maximum number of times that the **Client Secret** can be used together with the **Client ID** to get back an access token; a value of `0` implies infinite number of uses. @@ -113,10 +113,10 @@ using the Universal Auth authentication method. To access the Infisical API as the identity, you should first perform a login operation that is to exchange the **Client ID** and **Client Secret** of the identity for an access token by making a request to the `/api/v1/auth/universal-auth/login` endpoint. - + Choose the correct base URL based on your region: - + - For Infisical Cloud US users: `https://app.infisical.com` - For Infisical Cloud EU users: `https://eu.infisical.com` @@ -144,7 +144,7 @@ using the Universal Auth authentication method. Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) - Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + 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 in the Universal Auth configuration. If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, @@ -159,14 +159,14 @@ using the Universal Auth authentication method. There are a few reasons for why this might happen: - + - The client secret or access token has expired. - - The identity is insufficently permissioned to interact with the resources you wish to access. + - The identity is insufficiently permissioned to interact with the resources you wish to access. - The client secret/access token is being used from an untrusted IP. A identity access token can have a time-to-live (TTL) or incremental lifetime after which it expires. - + In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. A token can be renewed any number of times where each call to renew it can extend the token's lifetime by increments of the access token's TTL. diff --git a/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx b/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx index 1e8eb3950..c44d06d4a 100644 --- a/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx +++ b/docs/documentation/platform/secret-rotation/aws-iam-user-secret.mdx @@ -182,10 +182,10 @@ In the following steps, we explore the end-to-end workflow for setting up this s - There are a few reasons for why this might happen: + There are a few reasons for why this might happen: - The strategy configuration is invalid (e.g. the managing IAM user's credentials are incorrect, the target AWS region is incorrect, etc.) - - The managing IAM user is insufficently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup + - The managing IAM user is insufficiently permissioned to rotate the credentials of the target IAM user. For instance, you may have setup [paths](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) for the managing IAM user and the policy does not have the necessary - permissions to rotate the credentials. + permissions to rotate the credentials. diff --git a/docs/images/platform/identities/identities-org-create-oci-auth-method.png b/docs/images/platform/identities/identities-org-create-oci-auth-method.png new file mode 100644 index 000000000..6d08b4ee9 Binary files /dev/null and b/docs/images/platform/identities/identities-org-create-oci-auth-method.png differ diff --git a/docs/images/platform/identities/identities-org-create.png b/docs/images/platform/identities/identities-org-create.png index 06a1ef496..cf5b4c3a5 100644 Binary files a/docs/images/platform/identities/identities-org-create.png and b/docs/images/platform/identities/identities-org-create.png differ diff --git a/docs/images/platform/identities/identities-org.png b/docs/images/platform/identities/identities-org.png index ad75b3dd1..8d396ca84 100644 Binary files a/docs/images/platform/identities/identities-org.png and b/docs/images/platform/identities/identities-org.png differ diff --git a/docs/images/platform/identities/identities-page-remove-default-auth.png b/docs/images/platform/identities/identities-page-remove-default-auth.png index 5b8f22fa2..55c2fbf80 100644 Binary files a/docs/images/platform/identities/identities-page-remove-default-auth.png and b/docs/images/platform/identities/identities-page-remove-default-auth.png differ diff --git a/docs/images/platform/identities/identities-page.png b/docs/images/platform/identities/identities-page.png index 35b8af658..43692ea5d 100644 Binary files a/docs/images/platform/identities/identities-page.png and b/docs/images/platform/identities/identities-page.png differ diff --git a/docs/images/platform/identities/identities-press-cog.png b/docs/images/platform/identities/identities-press-cog.png new file mode 100644 index 000000000..08cd381af Binary files /dev/null and b/docs/images/platform/identities/identities-press-cog.png differ diff --git a/docs/images/platform/identities/identities-project-create.png b/docs/images/platform/identities/identities-project-create.png index d7a2cc5e1..49094fcac 100644 Binary files a/docs/images/platform/identities/identities-project-create.png and b/docs/images/platform/identities/identities-project-create.png differ diff --git a/docs/images/platform/identities/identities-project.png b/docs/images/platform/identities/identities-project.png index b02b7cfca..c561dc342 100644 Binary files a/docs/images/platform/identities/identities-project.png and b/docs/images/platform/identities/identities-project.png differ diff --git a/docs/mint.json b/docs/mint.json index 88868bd6a..61b89c609 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -300,14 +300,14 @@ { "group": "Machine Identities", "pages": [ + "documentation/platform/identities/aws-auth", + "documentation/platform/identities/azure-auth", + "documentation/platform/identities/gcp-auth", + "documentation/platform/identities/jwt-auth", + "documentation/platform/identities/kubernetes-auth", + "documentation/platform/identities/oci-auth", "documentation/platform/identities/token-auth", "documentation/platform/identities/universal-auth", - "documentation/platform/identities/kubernetes-auth", - "documentation/platform/identities/gcp-auth", - "documentation/platform/identities/azure-auth", - "documentation/platform/identities/aws-auth", - "documentation/platform/identities/jwt-auth", - { "group": "OIDC Auth", "pages": [ @@ -700,6 +700,16 @@ "api-reference/endpoints/aws-auth/revoke" ] }, + { + "group": "OCI Auth", + "pages": [ + "api-reference/endpoints/oci-auth/login", + "api-reference/endpoints/oci-auth/attach", + "api-reference/endpoints/oci-auth/retrieve", + "api-reference/endpoints/oci-auth/update", + "api-reference/endpoints/oci-auth/revoke" + ] + }, { "group": "Azure Auth", "pages": [ diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 97acd6dfc..71f70806a 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -7,6 +7,7 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { [IdentityAuthMethod.GCP_AUTH]: "GCP Auth", [IdentityAuthMethod.AWS_AUTH]: "AWS Auth", [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth", + [IdentityAuthMethod.OCI_AUTH]: "OCI Auth", [IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth", [IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth", [IdentityAuthMethod.JWT_AUTH]: "JWT Auth" diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 8a8d99fae..a9b6eb3e1 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -5,6 +5,7 @@ export enum IdentityAuthMethod { GCP_AUTH = "gcp-auth", AWS_AUTH = "aws-auth", AZURE_AUTH = "azure-auth", + OCI_AUTH = "oci-auth", OIDC_AUTH = "oidc-auth", LDAP_AUTH = "ldap-auth", JWT_AUTH = "jwt-auth" diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index e0077527f..a76b7ecac 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -11,6 +11,7 @@ import { AddIdentityJwtAuthDTO, AddIdentityKubernetesAuthDTO, AddIdentityLdapAuthDTO, + AddIdentityOciAuthDTO, AddIdentityOidcAuthDTO, AddIdentityTokenAuthDTO, AddIdentityUniversalAuthDTO, @@ -27,6 +28,7 @@ import { DeleteIdentityJwtAuthDTO, DeleteIdentityKubernetesAuthDTO, DeleteIdentityLdapAuthDTO, + DeleteIdentityOciAuthDTO, DeleteIdentityOidcAuthDTO, DeleteIdentityTokenAuthDTO, DeleteIdentityUniversalAuthClientSecretDTO, @@ -39,6 +41,7 @@ import { IdentityJwtAuth, IdentityKubernetesAuth, IdentityLdapAuth, + IdentityOciAuth, IdentityOidcAuth, IdentityTokenAuth, IdentityUniversalAuth, @@ -51,6 +54,7 @@ import { UpdateIdentityJwtAuthDTO, UpdateIdentityKubernetesAuthDTO, UpdateIdentityLdapAuthDTO, + UpdateIdentityOciAuthDTO, UpdateIdentityOidcAuthDTO, UpdateIdentityTokenAuthDTO, UpdateIdentityUniversalAuthDTO, @@ -452,6 +456,101 @@ export const useDeleteIdentityAwsAuth = () => { }); }; +export const useAddIdentityOciAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityOciAuth } + } = await apiRequest.post<{ identityOciAuth: IdentityOciAuth }>( + `/api/v1/auth/oci-auth/identities/${identityId}`, + { + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityOciAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOciAuth(identityId) }); + } + }); +}; + +export const useUpdateIdentityOciAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityOciAuth } + } = await apiRequest.patch<{ identityOciAuth: IdentityOciAuth }>( + `/api/v1/auth/oci-auth/identities/${identityId}`, + { + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityOciAuth; + }, + onSuccess: (_, { identityId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOciAuth(identityId) }); + } + }); +}; + +export const useDeleteIdentityOciAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ identityId }) => { + const { + data: { identityOciAuth } + } = await apiRequest.delete(`/api/v1/auth/oci-auth/identities/${identityId}`); + return identityOciAuth; + }, + onSuccess: (_, { organizationId, identityId }) => { + queryClient.invalidateQueries({ + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) + }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) }); + queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityOciAuth(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 3bc94534c..adc18ed6f 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -14,6 +14,7 @@ import { IdentityLdapAuth, IdentityMembership, IdentityMembershipOrg, + IdentityOciAuth, IdentityOidcAuth, IdentityTokenAuth, IdentityUniversalAuth, @@ -32,6 +33,7 @@ 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, + 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, getIdentityJwtAuth: (identityId: string) => [{ identityId }, "identity-jwt-auth"] as const, @@ -170,6 +172,27 @@ export const useGetIdentityAwsAuth = ( }); }; +export const useGetIdentityOciAuth = ( + identityId: string, + options?: TReactQueryOptions["options"] +) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityOciAuth(identityId), + queryFn: async () => { + const { + data: { identityOciAuth } + } = await apiRequest.get<{ identityOciAuth: IdentityOciAuth }>( + `/api/v1/auth/oci-auth/identities/${identityId}` + ); + return identityOciAuth; + }, + 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 c5f8cbc4a..df6a01499 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -290,6 +290,48 @@ export type DeleteIdentityAwsAuthDTO = { identityId: string; }; +export type IdentityOciAuth = { + identityId: string; + type: "iam"; + tenancyOcid: string; + allowedUsernames?: string | null; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityOciAuthDTO = { + organizationId: string; + identityId: string; + tenancyOcid: string; + allowedUsernames?: string | null; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityOciAuthDTO = { + organizationId: string; + identityId: string; + tenancyOcid?: string; + allowedUsernames?: string | null; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + +export type DeleteIdentityOciAuthDTO = { + organizationId: string; + identityId: string; +}; + export type IdentityAzureAuth = { identityId: string; tenantId: string; 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 0444b3bd1..8f619029d 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 @@ -14,6 +14,7 @@ import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm"; import { IdentityJwtAuthForm } from "./IdentityJwtAuthForm"; import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm"; import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm"; +import { IdentityOciAuthForm } from "./IdentityOciAuthForm"; import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm"; import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; @@ -46,6 +47,7 @@ const identityAuthMethods = [ { label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH }, { label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }, { label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }, + { label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH }, { label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }, { label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH }, { @@ -180,6 +182,16 @@ export const IdentityAuthMethodModalContent = ({ ) }, + [IdentityAuthMethod.OCI_AUTH]: { + render: () => ( + + ) + }, + [IdentityAuthMethod.JWT_AUTH]: { render: () => ( /^ocid1\.tenancy\.oc1\..+$/.test(val), + "Invalid Tenancy OCID format. Must start with ocid1.tenancy.oc1." + ), + allowedUsernames: z.string().optional(), + 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 IdentityOciAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityId, + isUpdate +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityOciAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityOciAuth(); + const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); + + const { data } = useGetIdentityOciAuth(identityId ?? "", { + enabled: isUpdate + }); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + tenancyOcid: "", + allowedUsernames: "", + 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({ + tenancyOcid: data.tenancyOcid, + allowedUsernames: data.allowedUsernames || undefined, + 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({ + tenancyOcid: "", + allowedUsernames: undefined, + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + tenancyOcid, + allowedUsernames, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityId) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + tenancyOcid, + allowedUsernames, + identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId, + tenancyOcid, + allowedUsernames: allowedUsernames || undefined, + 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/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx index 62039ceb9..71830d398 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx @@ -12,6 +12,7 @@ import { useDeleteIdentityJwtAuth, useDeleteIdentityKubernetesAuth, useDeleteIdentityLdapAuth, + useDeleteIdentityOciAuth, useDeleteIdentityOidcAuth, useDeleteIdentityTokenAuth, useDeleteIdentityUniversalAuth @@ -24,6 +25,7 @@ import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent"; import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent"; import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent"; import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent"; +import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent"; import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent"; import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent"; import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent"; @@ -61,6 +63,7 @@ export const Content = ({ const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); + const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth(); const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth(); @@ -95,6 +98,10 @@ export const Content = ({ revokeMethod = revokeAzureAuth; Component = ViewIdentityAzureAuthContent; break; + case IdentityAuthMethod.OCI_AUTH: + revokeMethod = revokeOciAuth; + Component = ViewIdentityOciAuthContent; + break; case IdentityAuthMethod.OIDC_AUTH: revokeMethod = revokeOidcAuth; Component = ViewIdentityOidcAuthContent; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx new file mode 100644 index 000000000..fb3bd4fa8 --- /dev/null +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx @@ -0,0 +1,73 @@ +import { faBan } from "@fortawesome/free-solid-svg-icons"; + +import { EmptyState, Spinner } from "@app/components/v2"; +import { useGetIdentityOciAuth } from "@app/hooks/api"; +import { IdentityOciAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm"; + +import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; +import { ViewAuthMethodProps } from "./types"; +import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; + +export const ViewIdentityOciAuthContent = ({ + identityId, + handlePopUpToggle, + handlePopUpOpen, + onDelete, + popUp +}: ViewAuthMethodProps) => { + const { data, isPending } = useGetIdentityOciAuth(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.tenancyOcid} + + + {data.allowedUsernames + ?.split(",") + .map((u) => u.trim()) + .join(", ")} + + + ); +}; diff --git a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx index 9790df292..55d4104ac 100644 --- a/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx +++ b/frontend/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx @@ -61,7 +61,7 @@ const schema = z allowCustomKeyIds: z.boolean().optional().default(false) }) .refine((data) => ms(data.maxTTL) >= ms(data.ttl), { - message: "Max TLL must be greater than or equal to TTL", + message: "Max TTL must be greater than or equal to TTL", path: ["maxTTL"] });