Compare commits

..

33 Commits

Author SHA1 Message Date
179edd98bf misc: rolled back frontend package-lock 2024-07-10 13:45:35 +08:00
dc05b34fb1 misc: rolled back package locks 2024-07-10 13:41:33 +08:00
20f6dbfbd1 Update oidc docs image 2024-07-10 12:09:24 +07:00
8ff524a037 Move migration file to front 2024-07-10 11:51:53 +07:00
3913e2f462 Fix merge conflicts, bring oidc auth up to speed with identity ui changes 2024-07-10 10:31:48 +07:00
ebbccdb857 add better label for identity id 2024-07-09 18:13:06 -04:00
df90e4e6f0 Update go version in k8s dockerfile 2024-07-09 09:44:52 -04:00
6e9a624697 Merge pull request #2090 from Infisical/daniel/operator-bump-sdk
fix(operator): azure auth
2024-07-09 09:32:39 -04:00
94b0cb4697 Bump helm 2024-07-09 15:31:47 +02:00
5a5226c82f Update values.yaml 2024-07-09 15:27:11 +02:00
09cfaec175 Update infisicalsecret_controller.go 2024-07-09 15:27:11 +02:00
40abc184f2 Fix: Bump SDK version 2024-07-09 15:27:11 +02:00
3879edfab7 Merge pull request #2089 from Infisical/docs-token-auth
Update identity docs for auth token and newer identity flow
2024-07-09 16:51:44 +07:00
53c875424e Update identity docs for auth token and newer identity flow 2024-07-09 16:47:24 +07:00
0eab9233bb Merge pull request #2076 from Infisical/misc/redesigned-org-security-settings
misc: redesigned org security settings page
2024-07-09 15:05:34 +08:00
9bf358a57d Merge pull request #2057 from Infisical/token-auth
Token Authentication Method + Revamped Identity (Page) Workflow
2024-07-09 11:55:56 +07:00
93926cc6b7 Merge remote-tracking branch 'origin' into token-auth 2024-07-09 11:52:14 +07:00
59ccabec69 Make fixes based on review 2024-07-09 11:51:21 +07:00
a37987b508 misc: added proper error handling 2024-07-08 21:31:11 +08:00
96e485910c Merge remote-tracking branch 'origin' into token-auth 2024-07-08 17:14:55 +07:00
b81f7d8350 Finish new identity page 2024-07-08 17:12:49 +07:00
d4a2f4590b misc: redesigned org security settings page 2024-07-06 00:25:27 +08:00
8bc6edd165 doc: added general docs for oidc auth 2024-07-04 22:31:03 +08:00
2497aada8a misc: added oidc auth to access token trusted Ips 2024-07-04 15:54:37 +08:00
5921f349a8 misc: removed comment 2024-07-04 12:42:36 +08:00
b5166f1d39 Identity redesign modal opt 2024-07-03 13:29:31 -07:00
4927cc804a feat: added endpoint for oidc auth revocation 2024-07-04 00:53:24 +08:00
2153dd94eb feat: finished up login with identity oidc 2024-07-03 23:48:31 +08:00
08322f46f9 misc: setup audit logs for oidc identity 2024-07-03 21:38:13 +08:00
fc9326272a feat: finished up oidc auth management 2024-07-03 21:18:50 +08:00
4cfe564f3d Fix lint issues 2024-07-02 15:15:45 -07:00
93be4095c0 Finish preliminary token auth method 2024-07-02 15:05:57 -07:00
c90e423e4a feat: initial setup 2024-07-03 02:06:02 +08:00
115 changed files with 7242 additions and 1752 deletions

View File

@ -52,6 +52,7 @@
"jmespath": "^0.16.0",
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"libsodium-wrappers": "^0.7.13",
@ -14094,6 +14095,43 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwks-rsa": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz",
"integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==",
"dependencies": {
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"debug": "^4.3.4",
"jose": "^4.14.6",
"limiter": "^1.1.5",
"lru-memoizer": "^2.2.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/jwks-rsa/node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/jwks-rsa/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
@ -14321,6 +14359,11 @@
"node": ">=14"
}
},
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -14395,6 +14438,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -14513,6 +14561,15 @@
"node": ">=10"
}
},
"node_modules/lru-memoizer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
"dependencies": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "6.0.0"
}
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",

View File

@ -125,8 +125,8 @@
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
"argon2": "^0.31.2",
@ -148,6 +148,7 @@
"jmespath": "^0.16.0",
"jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0",
"knex": "^3.0.1",
"ldapjs": "^3.0.7",
"libsodium-wrappers": "^0.7.13",

View File

@ -41,7 +41,9 @@ import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-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";
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
@ -128,11 +130,13 @@ declare module "fastify" {
identity: TIdentityServiceFactory;
identityAccessToken: TIdentityAccessTokenServiceFactory;
identityProject: TIdentityProjectServiceFactory;
identityTokenAuth: TIdentityTokenAuthServiceFactory;
identityUa: TIdentityUaServiceFactory;
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory;
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;

View File

@ -92,6 +92,9 @@ import {
TIdentityKubernetesAuths,
TIdentityKubernetesAuthsInsert,
TIdentityKubernetesAuthsUpdate,
TIdentityOidcAuths,
TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate,
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
@ -104,6 +107,9 @@ import {
TIdentityProjectMemberships,
TIdentityProjectMembershipsInsert,
TIdentityProjectMembershipsUpdate,
TIdentityTokenAuths,
TIdentityTokenAuthsInsert,
TIdentityTokenAuthsUpdate,
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,
TIdentityUaClientSecretsUpdate,
@ -450,6 +456,11 @@ declare module "knex/types/tables" {
TIntegrationAuthsUpdate
>;
[TableName.Identity]: KnexOriginal.CompositeTableType<TIdentities, TIdentitiesInsert, TIdentitiesUpdate>;
[TableName.IdentityTokenAuth]: KnexOriginal.CompositeTableType<
TIdentityTokenAuths,
TIdentityTokenAuthsInsert,
TIdentityTokenAuthsUpdate
>;
[TableName.IdentityUniversalAuth]: KnexOriginal.CompositeTableType<
TIdentityUniversalAuths,
TIdentityUniversalAuthsInsert,
@ -475,6 +486,11 @@ declare module "knex/types/tables" {
TIdentityAzureAuthsInsert,
TIdentityAzureAuthsUpdate
>;
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
TIdentityOidcAuths,
TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate
>;
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
TIdentityUaClientSecrets,
TIdentityUaClientSecretsInsert,

View File

@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.IdentityTokenAuth, (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");
});
await createOnUpdateTrigger(knex, TableName.IdentityTokenAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityTokenAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityTokenAuth);
}

View File

@ -0,0 +1,24 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.IdentityAccessToken)) {
const hasNameColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "name");
if (!hasNameColumn) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.string("name").nullable();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.IdentityAccessToken)) {
if (await knex.schema.hasColumn(TableName.IdentityAccessToken, "name")) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.dropColumn("name");
});
}
}
}

View File

@ -0,0 +1,34 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityOidcAuth))) {
await knex.schema.createTable(TableName.IdentityOidcAuth, (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.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("oidcDiscoveryUrl").notNullable();
t.text("encryptedCaCert").notNullable();
t.string("caCertIV").notNullable();
t.string("caCertTag").notNullable();
t.string("boundIssuer").notNullable();
t.string("boundAudiences").notNullable();
t.jsonb("boundClaims").notNullable();
t.string("boundSubject");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityOidcAuth);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityOidcAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityOidcAuth);
}

View File

@ -19,7 +19,8 @@ export const IdentityAccessTokensSchema = z.object({
identityUAClientSecretId: z.string().nullable().optional(),
identityId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
name: z.string().nullable().optional()
});
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;

View File

@ -0,0 +1,31 @@
// 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 IdentityOidcAuthsSchema = 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(),
identityId: z.string().uuid(),
oidcDiscoveryUrl: z.string(),
encryptedCaCert: z.string(),
caCertIV: z.string(),
caCertTag: z.string(),
boundIssuer: z.string(),
boundAudiences: z.string(),
boundClaims: z.unknown(),
boundSubject: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
export type TIdentityOidcAuthsInsert = Omit<z.input<typeof IdentityOidcAuthsSchema>, TImmutableDBKeys>;
export type TIdentityOidcAuthsUpdate = Partial<Omit<z.input<typeof IdentityOidcAuthsSchema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,23 @@
// 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 IdentityTokenAuthsSchema = 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()
});
export type TIdentityTokenAuths = z.infer<typeof IdentityTokenAuthsSchema>;
export type TIdentityTokenAuthsInsert = Omit<z.input<typeof IdentityTokenAuthsSchema>, TImmutableDBKeys>;
export type TIdentityTokenAuthsUpdate = Partial<Omit<z.input<typeof IdentityTokenAuthsSchema>, TImmutableDBKeys>>;

View File

@ -28,10 +28,12 @@ export * from "./identity-aws-auths";
export * from "./identity-azure-auths";
export * from "./identity-gcp-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-oidc-auths";
export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";
export * from "./identity-project-membership-role";
export * from "./identity-project-memberships";
export * from "./identity-token-auths";
export * from "./identity-ua-client-secrets";
export * from "./identity-universal-auths";
export * from "./incident-contacts";

View File

@ -53,12 +53,14 @@ export enum TableName {
Webhook = "webhooks",
Identity = "identities",
IdentityAccessToken = "identity_access_tokens",
IdentityTokenAuth = "identity_token_auths",
IdentityUniversalAuth = "identity_universal_auths",
IdentityKubernetesAuth = "identity_kubernetes_auths",
IdentityGcpAuth = "identity_gcp_auths",
IdentityAzureAuth = "identity_azure_auths",
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
IdentityOidcAuth = "identity_oidc_auths",
IdentityOrgMembership = "identity_org_memberships",
IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role",
@ -161,9 +163,11 @@ export enum ProjectUpgradeStatus {
}
export enum IdentityAuthMethod {
TOKEN_AUTH = "token-auth",
Univeral = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth"
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth"
}

View File

@ -45,7 +45,6 @@ export enum EventType {
CREATE_SECRETS = "create-secrets",
UPDATE_SECRET = "update-secret",
UPDATE_SECRETS = "update-secrets",
MOVE_SECRETS = "move-secrets",
DELETE_SECRET = "delete-secret",
DELETE_SECRETS = "delete-secrets",
GET_WORKSPACE_KEY = "get-workspace-key",
@ -67,11 +66,23 @@ export enum EventType {
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth",
CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth",
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth",
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth",
ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth",
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
@ -229,17 +240,6 @@ interface UpdateSecretBatchEvent {
};
}
interface MoveSecretsEvent {
type: EventType.MOVE_SECRETS;
metadata: {
sourceEnvironment: string;
sourceSecretPath: string;
destinationEnvironment: string;
destinationSecretPath: string;
secretIds: string[];
};
}
interface DeleteSecretEvent {
type: EventType.DELETE_SECRET;
metadata: {
@ -459,6 +459,66 @@ interface DeleteIdentityUniversalAuthEvent {
};
}
interface CreateTokenIdentityTokenAuthEvent {
type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
identityAccessTokenId: string;
};
}
interface UpdateTokenIdentityTokenAuthEvent {
type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
tokenId: string;
name?: string;
};
}
interface GetTokensIdentityTokenAuthEvent {
type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
};
}
interface AddIdentityTokenAuthEvent {
type: EventType.ADD_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface UpdateIdentityTokenAuthEvent {
type: EventType.UPDATE_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityTokenAuthEvent {
type: EventType.GET_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
};
}
interface DeleteIdentityTokenAuthEvent {
type: EventType.REVOKE_IDENTITY_TOKEN_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityKubernetesAuthEvent {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
metadata: {
@ -694,6 +754,63 @@ interface GetIdentityAzureAuthEvent {
};
}
interface LoginIdentityOidcAuthEvent {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
metadata: {
identityId: string;
identityOidcAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityOidcAuthEvent {
type: EventType.ADD_IDENTITY_OIDC_AUTH;
metadata: {
identityId: string;
oidcDiscoveryUrl: string;
caCert: string;
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityOidcAuthEvent {
type: EventType.REVOKE_IDENTITY_OIDC_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityOidcAuthEvent {
type: EventType.UPDATE_IDENTITY_OIDC_AUTH;
metadata: {
identityId: string;
oidcDiscoveryUrl?: string;
caCert?: string;
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityOidcAuthEvent {
type: EventType.GET_IDENTITY_OIDC_AUTH;
metadata: {
identityId: string;
};
}
interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT;
metadata: {
@ -1042,7 +1159,6 @@ export type Event =
| CreateSecretBatchEvent
| UpdateSecretEvent
| UpdateSecretBatchEvent
| MoveSecretsEvent
| DeleteSecretEvent
| DeleteSecretBatchEvent
| GetWorkspaceKeyEvent
@ -1064,6 +1180,13 @@ export type Event =
| UpdateIdentityUniversalAuthEvent
| DeleteIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| CreateTokenIdentityTokenAuthEvent
| UpdateTokenIdentityTokenAuthEvent
| GetTokensIdentityTokenAuthEvent
| AddIdentityTokenAuthEvent
| UpdateIdentityTokenAuthEvent
| GetIdentityTokenAuthEvent
| DeleteIdentityTokenAuthEvent
| LoginIdentityKubernetesAuthEvent
| DeleteIdentityKubernetesAuthEvent
| AddIdentityKubernetesAuthEvent
@ -1088,6 +1211,11 @@ export type Event =
| DeleteIdentityAzureAuthEvent
| UpdateIdentityAzureAuthEvent
| GetIdentityAzureAuthEvent
| LoginIdentityOidcAuthEvent
| AddIdentityOidcAuthEvent
| DeleteIdentityOidcAuthEvent
| UpdateIdentityOidcAuthEvent
| GetIdentityOidcAuthEvent
| CreateEnvironmentEvent
| UpdateEnvironmentEvent
| DeleteEnvironmentEvent

View File

@ -142,6 +142,12 @@ export const KUBERNETES_AUTH = {
}
} as const;
export const OIDC_AUTH = {
REVOKE: {
identityId: "The ID of the identity to revoke."
}
} as const;
export const ORGANIZATIONS = {
LIST_USER_MEMBERSHIPS: {
organizationId: "The ID of the organization to get memberships from."

View File

@ -102,9 +102,13 @@ import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/ident
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-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";
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
@ -234,11 +238,13 @@ export const registerRoutes = async (
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityTokenAuthDAL = identityTokenAuthDALFactory(db);
const identityUaDAL = identityUaDALFactory(db);
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
const auditLogDAL = auditLogDALFactory(db);
@ -706,10 +712,7 @@ export const registerRoutes = async (
secretQueueService,
secretImportDAL,
projectEnvDAL,
projectBotService,
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL
projectBotService
});
const secretSharingService = secretSharingServiceFactory({
@ -811,6 +814,7 @@ export const registerRoutes = async (
permissionService,
identityDAL,
identityOrgMembershipDAL,
identityProjectDAL,
licenseService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({
@ -831,6 +835,14 @@ export const registerRoutes = async (
permissionService,
identityProjectDAL
});
const identityTokenAuthService = identityTokenAuthServiceFactory({
identityTokenAuthDAL,
identityDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
permissionService,
licenseService
});
const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL,
permissionService,
@ -876,6 +888,16 @@ export const registerRoutes = async (
licenseService
});
const identityOidcAuthService = identityOidcAuthServiceFactory({
identityOidcAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
permissionService,
licenseService,
orgBotDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService,
@ -973,11 +995,13 @@ export const registerRoutes = async (
identity: identityService,
identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService,
identityTokenAuth: identityTokenAuthService,
identityUa: identityUaService,
identityKubernetesAuth: identityKubernetesAuthService,
identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService,
identityOidcAuth: identityOidcAuthService,
accessApprovalPolicy: accessApprovalPolicyService,
accessApprovalRequest: accessApprovalRequestService,
secretApprovalPolicy: secretApprovalPolicyService,

View File

@ -0,0 +1,350 @@
import { z } from "zod";
import { IdentityOidcAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { OIDC_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 {
validateOidcAuthAudiencesField,
validateOidcBoundClaimsField
} from "@app/services/identity-oidc-auth/identity-oidc-auth-validators";
const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.omit({
encryptedCaCert: true,
caCertIV: true,
caCertTag: true
}).extend({
caCert: z.string()
});
export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/oidc-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
description: "Login with OIDC Auth",
body: z.object({
identityId: z.string().trim(),
jwt: z.string().trim()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityOidcAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH,
metadata: {
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityOidcAuthId: identityOidcAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/oidc-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach OIDC Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
accessTokenTTL: z
.number()
.int()
.min(1)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000),
accessTokenNumUsesLimit: z.number().int().min(0).default(0),
oidcDiscoveryUrl: z.string().url().min(1),
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: validateOidcAuthAudiencesField,
boundClaims: validateOidcBoundClaimsField,
boundSubject: z.string().optional().default("")
}),
response: {
200: z.object({
identityOidcAuth: IdentityOidcAuthResponseSchema
})
}
},
handler: async (req) => {
const identityOidcAuth = await server.services.identityOidcAuth.attachOidcAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOidcAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_OIDC_AUTH,
metadata: {
identityId: identityOidcAuth.identityId,
oidcDiscoveryUrl: identityOidcAuth.oidcDiscoveryUrl,
caCert: identityOidcAuth.caCert,
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOidcAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit
}
}
});
return {
identityOidcAuth
};
}
});
server.route({
method: "PATCH",
url: "/oidc-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update OIDC Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z
.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
accessTokenTTL: z
.number()
.int()
.min(1)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000),
accessTokenNumUsesLimit: z.number().int().min(0).default(0),
oidcDiscoveryUrl: z.string().url().min(1),
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: validateOidcAuthAudiencesField,
boundClaims: validateOidcBoundClaimsField,
boundSubject: z.string().optional().default("")
})
.partial(),
response: {
200: z.object({
identityOidcAuth: IdentityOidcAuthResponseSchema
})
}
},
handler: async (req) => {
const identityOidcAuth = await server.services.identityOidcAuth.updateOidcAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOidcAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_OIDC_AUTH,
metadata: {
identityId: identityOidcAuth.identityId,
oidcDiscoveryUrl: identityOidcAuth.oidcDiscoveryUrl,
caCert: identityOidcAuth.caCert,
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOidcAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit
}
}
});
return { identityOidcAuth };
}
});
server.route({
method: "GET",
url: "/oidc-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve OIDC Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityOidcAuth: IdentityOidcAuthResponseSchema
})
}
},
handler: async (req) => {
const identityOidcAuth = await server.services.identityOidcAuth.getOidcAuth({
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: identityOidcAuth.orgId,
event: {
type: EventType.GET_IDENTITY_OIDC_AUTH,
metadata: {
identityId: identityOidcAuth.identityId
}
}
});
return { identityOidcAuth };
}
});
server.route({
method: "DELETE",
url: "/oidc-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete OIDC Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OIDC_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityOidcAuth: IdentityOidcAuthResponseSchema.omit({
caCert: true
})
})
}
},
handler: async (req) => {
const identityOidcAuth = await server.services.identityOidcAuth.revokeOidcAuth({
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: identityOidcAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_OIDC_AUTH,
metadata: {
identityId: identityOidcAuth.identityId
}
}
});
return { identityOidcAuth };
}
});
};

View File

@ -1,6 +1,12 @@
import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import {
IdentitiesSchema,
IdentityOrgMembershipsSchema,
OrgMembershipRole,
OrgRolesSchema,
ProjectsSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@ -260,4 +266,63 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
return { identities };
}
});
server.route({
method: "GET",
url: "/:identityId/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List project memberships that identity with id is part of",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identityMemberships: z.array(
z.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: ProjectsSchema.pick({ name: true, id: true })
})
)
})
}
},
handler: async (req) => {
const identityMemberships = await server.services.identity.listProjectIdentitiesByIdentityId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
return { identityMemberships };
}
});
};

View File

@ -0,0 +1,468 @@
import { z } from "zod";
import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
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";
export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Attach Token Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
accessTokenTTL: z
.number()
.int()
.min(1)
.refine((value) => value !== 0, {
message: "accessTokenTTL must have a non zero number"
})
.default(2592000),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.default(2592000),
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.attachTokenAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityTokenAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId,
accessTokenTTL: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityTokenAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit
}
}
});
return {
identityTokenAuth
};
}
});
server.route({
method: "PATCH",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Token Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional(),
accessTokenTTL: z.number().int().min(0).optional(),
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
accessTokenMaxTTL: z
.number()
.int()
.refine((value) => value !== 0, {
message: "accessTokenMaxTTL must have a non zero number"
})
.optional()
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.updateTokenAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityTokenAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId,
accessTokenTTL: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityTokenAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit
}
}
});
return {
identityTokenAuth
};
}
});
server.route({
method: "GET",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Retrieve Token Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.getTokenAuth({
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: identityTokenAuth.orgId,
event: {
type: EventType.GET_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId
}
}
});
return { identityTokenAuth };
}
});
server.route({
method: "DELETE",
url: "/token-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete Token Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
response: {
200: z.object({
identityTokenAuth: IdentityTokenAuthsSchema
})
}
},
handler: async (req) => {
const identityTokenAuth = await server.services.identityTokenAuth.revokeIdentityTokenAuth({
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: identityTokenAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId
}
}
});
return { identityTokenAuth };
}
});
// proposed
// update token by id: PATCH /token-auth/tokens/:tokenId
// revoke token by id: POST /token-auth/tokens/:tokenId/revoke
// current
// revoke token by id: POST /token/revoke-by-id
// token-auth/identities/:identityId/tokens
server.route({
method: "POST",
url: "/token-auth/identities/:identityId/tokens",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create token for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
body: z.object({
name: z.string().optional()
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityTokenAuth.createTokenAuthToken({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.orgId,
event: {
type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: identityTokenAuth.identityId,
identityAccessTokenId: identityAccessToken.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "GET",
url: "/token-auth/identities/:identityId/tokens",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get tokens for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string()
}),
querystring: z.object({
offset: z.coerce.number().min(0).max(100).default(0),
limit: z.coerce.number().min(1).max(100).default(20)
}),
response: {
200: z.object({
tokens: IdentityAccessTokensSchema.array()
})
}
},
handler: async (req) => {
const { tokens, identityMembershipOrg } = await server.services.identityTokenAuth.getTokenAuthTokens({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.orgId,
event: {
type: EventType.GET_TOKENS_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: req.params.identityId
}
}
});
return { tokens };
}
});
server.route({
method: "PATCH",
url: "/token-auth/tokens/:tokenId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update token for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
tokenId: z.string()
}),
body: z.object({
name: z.string().optional()
}),
response: {
200: z.object({
token: IdentityAccessTokensSchema
})
}
},
handler: async (req) => {
const { token, identityMembershipOrg } = await server.services.identityTokenAuth.updateTokenAuthToken({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg.orgId,
event: {
type: EventType.UPDATE_TOKEN_IDENTITY_TOKEN_AUTH,
metadata: {
identityId: token.identityId,
tokenId: token.id,
name: req.body.name
}
}
});
return { token };
}
});
server.route({
method: "POST",
url: "/token-auth/tokens/:tokenId/revoke",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Revoke token for identity with Token Auth configured",
security: [
{
bearerAuth: []
}
],
params: z.object({
tokenId: z.string()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.identityTokenAuth.revokeTokenAuthToken({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
tokenId: req.params.tokenId
});
return {
message: "Successfully revoked access token"
};
}
});
};

View File

@ -8,7 +8,9 @@ import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router";
@ -34,12 +36,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(
async (authRouter) => {
await authRouter.register(registerAuthRoutes);
await authRouter.register(registerIdentityTokenAuthRouter);
await authRouter.register(registerIdentityUaRouter);
await authRouter.register(registerIdentityKubernetesRouter);
await authRouter.register(registerIdentityGcpAuthRouter);
await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter);
},
{ prefix: "/auth" }
);

View File

@ -1325,61 +1325,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/move",
config: {
rateLimit: secretsLimit
},
schema: {
body: z.object({
projectSlug: z.string().trim(),
sourceEnvironment: z.string().trim(),
sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
destinationEnvironment: z.string().trim(),
destinationSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
secretIds: z.string().array(),
shouldOverwrite: z.boolean().default(false)
}),
response: {
200: z.object({
isSourceUpdated: z.boolean(),
isDestinationUpdated: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { projectId, isSourceUpdated, isDestinationUpdated } = await server.services.secret.moveSecrets({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.MOVE_SECRETS,
metadata: {
sourceEnvironment: req.body.sourceEnvironment,
sourceSecretPath: req.body.sourceSecretPath,
destinationEnvironment: req.body.destinationEnvironment,
destinationSecretPath: req.body.destinationSecretPath,
secretIds: req.body.secretIds
}
}
});
return {
isSourceUpdated,
isDestinationUpdated
};
}
});
server.route({
method: "POST",
url: "/batch",

View File

@ -51,6 +51,12 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
`${TableName.IdentityKubernetesAuth}.identityId`
);
})
.leftJoin(TableName.IdentityOidcAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.OIDC_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityOidcAuth}.identityId`
);
})
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
@ -58,6 +64,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.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("name").withSchema(TableName.Identity)
)
.first();
@ -71,7 +78,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
doc.accessTokenTrustedIpsGcp ||
doc.accessTokenTrustedIpsAws ||
doc.accessTokenTrustedIpsAzure ||
doc.accessTokenTrustedIpsK8s
doc.accessTokenTrustedIpsK8s ||
doc.accessTokenTrustedIpsOidc
};
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });

View File

@ -131,7 +131,10 @@ export const identityAccessTokenServiceFactory = ({
});
if (!identityAccessToken) throw new UnauthorizedError();
const revokedToken = await identityAccessTokenDAL.deleteById(identityAccessToken.id);
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
isAccessTokenRevoked: true
});
return { revokedToken };
};
@ -141,6 +144,10 @@ export const identityAccessTokenServiceFactory = ({
isAccessTokenRevoked: false
});
if (!identityAccessToken) throw new UnauthorizedError();
if (identityAccessToken.isAccessTokenRevoked)
throw new UnauthorizedError({
message: "Failed to authorize revoked access token"
});
if (ipAddress && identityAccessToken) {
checkIPAgainstBlocklist({

View File

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityOidcAuthDALFactory = ReturnType<typeof identityOidcAuthDALFactory>;
export const identityOidcAuthDALFactory = (db: TDbClient) => {
const oidcAuthOrm = ormify(db, TableName.IdentityOidcAuth);
return oidcAuthOrm;
};

View File

@ -0,0 +1,534 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import https from "https";
import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import {
decryptSymmetric,
encryptSymmetric,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
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 { TOrgBotDALFactory } from "../org/org-bot-dal";
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
import {
TAttachOidcAuthDTO,
TGetOidcAuthDTO,
TLoginOidcAuthDTO,
TRevokeOidcAuthDTO,
TUpdateOidcAuthDTO
} from "./identity-oidc-auth-types";
type TIdentityOidcAuthServiceFactoryDep = {
identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
};
export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuthServiceFactory>;
export const identityOidcAuthServiceFactory = ({
identityOidcAuthDAL,
identityOrgMembershipDAL,
identityDAL,
permissionService,
licenseService,
identityAccessTokenDAL,
orgBotDAL
}: TIdentityOidcAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) {
throw new UnauthorizedError();
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
identityId: identityOidcAuth.identityId
});
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
}
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
}
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth;
let caCert = "";
if (encryptedCaCert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCaCert,
iv: caCertIV,
tag: caCertTag,
key
});
}
const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
httpsAgent: requestAgent
}
);
const jwksUri = discoveryDoc.jwks_uri;
const decodedToken = jwt.decode(oidcJwt, { complete: true });
if (!decodedToken) {
throw new BadRequestError({
message: "Invalid JWT"
});
}
const client = new JwksClient({
jwksUri,
requestAgent
});
const { kid } = decodedToken.header;
const oidcSigningKey = await client.getSigningKey(kid);
const tokenData = jwt.verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>;
if (identityOidcAuth.boundSubject) {
if (tokenData.sub !== identityOidcAuth.boundSubject) {
throw new UnauthorizedError();
}
}
if (identityOidcAuth.boundAudiences) {
if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) {
throw new UnauthorizedError();
}
}
if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) {
throw new UnauthorizedError();
}
});
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOidcAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg };
};
const attachOidcAuth = async ({
identityId,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
}
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add OIDC 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(OrgPermissionActions.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 orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: identityMembershipOrg.orgId,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
const doc = await identityOidcAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
oidcDiscoveryUrl,
encryptedCaCert,
caCertIV,
caCertTag,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.OIDC_AUTH
},
tx
);
return doc;
});
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
};
const updateOidcAuth = async ({
identityId,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
throw new BadRequestError({
message: "Failed to update OIDC Auth"
});
}
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityOidcAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityOidcAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityOidcAuth.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(OrgPermissionActions.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 updateQuery: TIdentityOidcAuthsUpdate = {
oidcDiscoveryUrl,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
};
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
}
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
if (caCert !== undefined) {
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
updateQuery.encryptedCaCert = encryptedCACert;
updateQuery.caCertIV = caCertIV;
updateQuery.caCertTag = caCertTag;
}
const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery);
const updatedCACert =
updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag
? decryptSymmetric({
ciphertext: updatedOidcAuth.encryptedCaCert,
iv: updatedOidcAuth.caCertIV,
tag: updatedOidcAuth.caCertTag,
key
})
: "";
return {
...updatedOidcAuth,
orgId: identityMembershipOrg.orgId,
caCert: updatedCACert
};
};
const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
throw new BadRequestError({
message: "The identity does not have OIDC Auth attached"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
}
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const caCert = decryptSymmetric({
ciphertext: identityOidcAuth.encryptedCaCert,
iv: identityOidcAuth.caCertIV,
tag: identityOidcAuth.caCertTag,
key
});
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
};
const revokeOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TRevokeOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) {
throw new BadRequestError({ message: "Failed to find identity" });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) {
throw new BadRequestError({
message: "The identity does not have OIDC auth"
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge) {
throw new ForbiddenRequestError({
message: "Failed to revoke OIDC auth of identity with more privileged role"
});
}
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityOidcAuth;
};
return {
attachOidcAuth,
updateOidcAuth,
getOidcAuth,
revokeOidcAuth,
login
};
};

View File

@ -0,0 +1,42 @@
import { TProjectPermission } from "@app/lib/types";
export type TAttachOidcAuthDTO = {
identityId: string;
oidcDiscoveryUrl: string;
caCert: string;
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOidcAuthDTO = {
identityId: string;
oidcDiscoveryUrl?: string;
caCert?: string;
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetOidcAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TLoginOidcAuthDTO = {
identityId: string;
jwt: string;
};
export type TRevokeOidcAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -0,0 +1,25 @@
import { z } from "zod";
export const validateOidcAuthAudiencesField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
return data
.split(",")
.map((id) => id.trim())
.join(", ");
});
export const validateOidcBoundClaimsField = z.record(z.string()).transform((data) => {
const formattedClaims: Record<string, string> = {};
Object.keys(data).forEach((key) => {
formattedClaims[key] = data[key]
.split(",")
.map((id) => id.trim())
.join(", ");
});
return formattedClaims;
});

View File

@ -10,6 +10,103 @@ export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFac
export const identityProjectDALFactory = (db: TDbClient) => {
const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership);
const findByIdentityId = async (identityId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.leftJoin(
TableName.IdentityProjectAdditionalPrivilege,
`${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
)
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("name").withSchema(TableName.ProjectRoles).as("customRoleName"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("temporaryMode").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project)
);
const members = sqlNestRelationships({
data: docs,
parentMapper: ({ identityName, identityAuthMethod, id, createdAt, updatedAt, projectId, projectName }) => ({
id,
identityId,
createdAt,
updatedAt,
identity: {
id: identityId,
name: identityName,
authMethod: identityAuthMethod
},
project: {
id: projectId,
name: projectName
}
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
customRoleName,
customRoleSlug,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName,
customRoleSlug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members;
} catch (error) {
throw new DatabaseError({ error, name: "FindByIdentityId" });
}
};
const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
@ -105,5 +202,9 @@ export const identityProjectDALFactory = (db: TDbClient) => {
}
};
return { ...identityProjectOrm, findByProjectId };
return {
...identityProjectOrm,
findByIdentityId,
findByProjectId
};
};

View File

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityTokenAuthDALFactory = ReturnType<typeof identityTokenAuthDALFactory>;
export const identityTokenAuthDALFactory = (db: TDbClient) => {
const tokenAuthOrm = ormify(db, TableName.IdentityTokenAuth);
return tokenAuthOrm;
};

View File

@ -0,0 +1,470 @@
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
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 { TIdentityTokenAuthDALFactory } from "./identity-token-auth-dal";
import {
TAttachTokenAuthDTO,
TCreateTokenAuthTokenDTO,
TGetTokenAuthDTO,
TGetTokenAuthTokensDTO,
TRevokeTokenAuthDTO,
TRevokeTokenAuthTokenDTO,
TUpdateTokenAuthDTO,
TUpdateTokenAuthTokenDTO
} from "./identity-token-auth-types";
type TIdentityTokenAuthServiceFactoryDep = {
identityTokenAuthDAL: Pick<
TIdentityTokenAuthDALFactory,
"transaction" | "create" | "findOne" | "updateById" | "delete"
>;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<
TIdentityAccessTokenDALFactory,
"create" | "find" | "update" | "findById" | "findOne" | "updateById"
>;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAuthServiceFactory>;
export const identityTokenAuthServiceFactory = ({
identityTokenAuthDAL,
identityDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
permissionService,
licenseService
}: TIdentityTokenAuthServiceFactoryDep) => {
const attachTokenAuth = async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add Token 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(OrgPermissionActions.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 identityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const doc = await identityTokenAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
tx
);
return doc;
});
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
};
const updateTokenAuth = async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "Failed to update Token Auth"
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityTokenAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityTokenAuth.accessTokenMaxTTL) >
(accessTokenMaxTTL || identityTokenAuth.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(OrgPermissionActions.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 updatedTokenAuth = await identityTokenAuthDAL.updateById(identityTokenAuth.id, {
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return {
...updatedTokenAuth,
orgId: identityMembershipOrg.orgId
};
};
const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth attached"
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityTokenAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new UnauthorizedError({
message: "Failed to revoke Token Auth of identity with more privileged role"
});
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityTokenAuth;
};
const createTokenAuthToken = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId,
name
}: TCreateTokenAuthTokenDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to create token for identity with more privileged role"
});
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityTokenAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityTokenAuth.accessTokenTTL,
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit,
name
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityTokenAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg };
};
const getTokenAuthTokens = async ({
identityId,
offset = 0,
limit = 20,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetTokenAuthTokensDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const tokens = await identityAccessTokenDAL.find(
{
identityId
},
{ offset, limit, sort: [["updatedAt", "desc"]] }
);
return { tokens, identityMembershipOrg };
};
const updateTokenAuthToken = async ({
tokenId,
name,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findById(tokenId);
if (!foundToken) throw new NotFoundError({ message: "Failed to find token" });
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
throw new BadRequestError({
message: "The identity does not have Token Auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to update token for identity with more privileged role"
});
const [token] = await identityAccessTokenDAL.update(
{
identityId: foundToken.identityId,
id: tokenId
},
{
name
}
);
return { token, identityMembershipOrg };
};
const revokeTokenAuthToken = async ({
tokenId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false
});
if (!identityAccessToken)
throw new NotFoundError({
message: "Failed to find token"
});
const identityOrgMembership = await identityOrgMembershipDAL.findOne({
identityId: identityAccessToken.identityId
});
if (!identityOrgMembership) {
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityOrgMembership.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
isAccessTokenRevoked: true
});
return { revokedToken };
};
return {
attachTokenAuth,
updateTokenAuth,
getTokenAuth,
revokeIdentityTokenAuth,
createTokenAuthToken,
getTokenAuthTokens,
updateTokenAuthToken,
revokeTokenAuthToken
};
};

View File

@ -0,0 +1,45 @@
import { TProjectPermission } from "@app/lib/types";
export type TAttachTokenAuthDTO = {
identityId: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthDTO = {
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeTokenAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateTokenAuthTokenDTO = {
identityId: string;
name?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTokenAuthTokensDTO = {
identityId: string;
offset: number;
limit: number;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateTokenAuthTokenDTO = {
tokenId: string;
name?: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeTokenAuthTokenDTO = {
tokenId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -5,17 +5,25 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, TUpdateIdentityDTO } from "./identity-types";
import {
TCreateIdentityDTO,
TDeleteIdentityDTO,
TGetIdentityByIdDTO,
TListProjectIdentitiesByIdentityIdDTO,
TUpdateIdentityDTO
} from "./identity-types";
type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
};
@ -25,6 +33,7 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
export const identityServiceFactory = ({
identityDAL,
identityOrgMembershipDAL,
identityProjectDAL,
permissionService,
licenseService
}: TIdentityServiceFactoryDep) => {
@ -196,11 +205,35 @@ export const identityServiceFactory = ({
return identityMemberships;
};
const listProjectIdentitiesByIdentityId = async ({
identityId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TListProjectIdentitiesByIdentityIdDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityOrgMembership.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityProjectDAL.findByIdentityId(identityId);
return identityMemberships;
};
return {
createIdentity,
updateIdentity,
deleteIdentity,
listOrgIdentities,
getIdentityById
getIdentityById,
listProjectIdentitiesByIdentityId
};
};

View File

@ -25,3 +25,7 @@ export interface TIdentityTrustedIp {
type: IPType;
prefix: number;
}
export type TListProjectIdentitiesByIdentityIdDTO = {
identityId: string;
} & Omit<TOrgPermission, "orgId">;

View File

@ -11,9 +11,6 @@ import {
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { getConfig } from "@app/lib/config/env";
import {
@ -21,10 +18,9 @@ import {
decryptSymmetric128BitHexKeyUTF8,
encryptSymmetric128BitHexKeyUTF8
} from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
@ -48,7 +44,6 @@ import {
} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue";
import {
SecretOperations,
TAttachSecretTagsDTO,
TBackFillSecretReferencesDTO,
TCreateBulkSecretDTO,
@ -64,7 +59,6 @@ import {
TGetSecretsDTO,
TGetSecretsRawDTO,
TGetSecretVersionsDTO,
TMoveSecretsDTO,
TUpdateBulkSecretDTO,
TUpdateManySecretRawDTO,
TUpdateSecretDTO,
@ -90,12 +84,6 @@ type TSecretServiceFactoryDep = {
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
secretApprovalRequestSecretDAL: Pick<
TSecretApprovalRequestSecretDALFactory,
"insertMany" | "insertApprovalSecretTags"
>;
};
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
@ -112,10 +100,7 @@ export const secretServiceFactory = ({
projectDAL,
projectBotService,
secretImportDAL,
secretVersionTagDAL,
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL
secretVersionTagDAL
}: TSecretServiceFactoryDep) => {
const getSecretReference = async (projectId: string) => {
// if bot key missing means e2e still exist
@ -1698,393 +1683,6 @@ export const secretServiceFactory = ({
return { message: "Successfully backfilled secret references" };
};
const moveSecrets = async ({
sourceEnvironment,
sourceSecretPath,
destinationEnvironment,
destinationSecretPath,
secretIds,
projectSlug,
shouldOverwrite,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TMoveSecretsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) {
throw new NotFoundError({
message: "Project not found."
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath })
);
const botKey = await projectBotService.getBotKey(project.id);
if (!botKey) {
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
}
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);
if (!sourceFolder) {
throw new NotFoundError({
message: "Source path does not exist."
});
}
const destinationFolder = await folderDAL.findBySecretPath(
project.id,
destinationEnvironment,
destinationSecretPath
);
if (!destinationFolder) {
throw new NotFoundError({
message: "Destination path does not exist."
});
}
const sourceSecrets = await secretDAL.find({
type: SecretType.Shared,
$in: {
id: secretIds
}
});
if (sourceSecrets.length !== secretIds.length) {
throw new BadRequestError({
message: "Invalid secrets"
});
}
const decryptedSourceSecrets = sourceSecrets.map((secret) => ({
...secret,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
}),
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
}));
let isSourceUpdated = false;
let isDestinationUpdated = false;
// Moving secrets is a two-step process.
await secretDAL.transaction(async (tx) => {
// First step is to create/update the secret in the destination:
const destinationSecretsFromDB = await secretDAL.find(
{
folderId: destinationFolder.id
},
{ tx }
);
const decryptedDestinationSecrets = destinationSecretsFromDB.map((secret) => {
return {
...secret,
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
}),
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
};
});
const destinationSecretsGroupedByBlindIndex = groupBy(
decryptedDestinationSecrets.filter(({ secretBlindIndex }) => Boolean(secretBlindIndex)),
(i) => i.secretBlindIndex as string
);
const locallyCreatedSecrets = decryptedSourceSecrets
.filter(({ secretBlindIndex }) => !destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0])
.map((el) => ({ ...el, operation: SecretOperations.Create }));
const locallyUpdatedSecrets = decryptedSourceSecrets
.filter(
({ secretBlindIndex, secretKey, secretValue }) =>
destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0] &&
// if key or value changed
(destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretKey !== secretKey ||
destinationSecretsGroupedByBlindIndex[secretBlindIndex as string]?.[0]?.secretValue !== secretValue)
)
.map((el) => ({ ...el, operation: SecretOperations.Update }));
if (locallyUpdatedSecrets.length > 0 && !shouldOverwrite) {
const existingKeys = locallyUpdatedSecrets.map((s) => s.secretKey);
throw new BadRequestError({
message: `Failed to move secrets. The following secrets already exist in the destination: ${existingKeys.join(
","
)}`
});
}
const isEmpty = locallyCreatedSecrets.length + locallyUpdatedSecrets.length === 0;
if (isEmpty) {
throw new BadRequestError({
message: "Selected secrets already exist in the destination."
});
}
const destinationFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
project.id,
destinationFolder.environment.slug,
destinationFolder.path
);
if (destinationFolderPolicy && actor === ActorType.USER) {
// if secret approval policy exists for destination, we create the secret approval request
const localSecretsIds = decryptedDestinationSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
destinationFolder.id,
localSecretsIds,
tx
);
const approvalRequestDoc = await secretApprovalRequestDAL.create(
{
folderId: destinationFolder.id,
slug: alphaNumericNanoId(),
policyId: destinationFolderPolicy.id,
status: "open",
hasMerged: false,
committerUserId: actorId
},
tx
);
const commits = locallyCreatedSecrets.concat(locallyUpdatedSecrets).map((doc) => {
const { operation } = doc;
const localSecret = destinationSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0];
return {
op: operation,
keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag,
secretKeyCiphertext: doc.secretKeyCiphertext,
secretValueIV: doc.secretValueIV,
secretValueTag: doc.secretValueTag,
secretValueCiphertext: doc.secretValueCiphertext,
secretBlindIndex: doc.secretBlindIndex,
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding,
// except create operation other two needs the secret id and version id
...(operation !== SecretOperations.Create
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
: {})
};
});
await secretApprovalRequestSecretDAL.insertMany(commits, tx);
} else {
// apply changes directly
if (locallyCreatedSecrets.length) {
await fnSecretBulkInsert({
folderId: destinationFolder.id,
secretVersionDAL,
secretDAL,
tx,
secretTagDAL,
secretVersionTagDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm,
type: doc.type,
metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag,
secretKeyCiphertext: doc.secretKeyCiphertext,
secretValueIV: doc.secretValueIV,
secretValueTag: doc.secretValueTag,
secretValueCiphertext: doc.secretValueCiphertext,
secretBlindIndex: doc.secretBlindIndex,
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding
};
})
});
}
if (locallyUpdatedSecrets.length) {
await fnSecretBulkUpdate({
projectId: project.id,
folderId: destinationFolder.id,
secretVersionDAL,
secretDAL,
tx,
secretTagDAL,
secretVersionTagDAL,
inputSecrets: locallyUpdatedSecrets.map((doc) => {
return {
filter: {
folderId: destinationFolder.id,
id: destinationSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
},
data: {
keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm,
type: doc.type,
metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag,
secretKeyCiphertext: doc.secretKeyCiphertext,
secretValueIV: doc.secretValueIV,
secretValueTag: doc.secretValueTag,
secretValueCiphertext: doc.secretValueCiphertext,
secretBlindIndex: doc.secretBlindIndex,
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding
}
};
})
});
}
isDestinationUpdated = true;
}
// Next step is to delete the secrets from the source folder:
const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string);
const locallyDeletedSecrets = decryptedSourceSecrets.map((el) => ({ ...el, operation: SecretOperations.Delete }));
const sourceFolderPolicy = await secretApprovalPolicyService.getSecretApprovalPolicy(
project.id,
sourceFolder.environment.slug,
sourceFolder.path
);
if (sourceFolderPolicy && actor === ActorType.USER) {
// if secret approval policy exists for source, we create the secret approval request
const localSecretsIds = decryptedSourceSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(sourceFolder.id, localSecretsIds, tx);
const approvalRequestDoc = await secretApprovalRequestDAL.create(
{
folderId: sourceFolder.id,
slug: alphaNumericNanoId(),
policyId: sourceFolderPolicy.id,
status: "open",
hasMerged: false,
committerUserId: actorId
},
tx
);
const commits = locallyDeletedSecrets.map((doc) => {
const { operation } = doc;
const localSecret = sourceSecretsGroupByBlindIndex[doc.secretBlindIndex as string]?.[0];
return {
op: operation,
keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag,
secretKeyCiphertext: doc.secretKeyCiphertext,
secretValueIV: doc.secretValueIV,
secretValueTag: doc.secretValueTag,
secretValueCiphertext: doc.secretValueCiphertext,
secretBlindIndex: doc.secretBlindIndex,
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretId: localSecret.id,
secretVersion: latestSecretVersions[localSecret.id].id
};
});
await secretApprovalRequestSecretDAL.insertMany(commits, tx);
} else {
// if no secret approval policy is present, we delete directly.
await secretDAL.delete(
{
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)
},
folderId: sourceFolder.id
},
tx
);
isSourceUpdated = true;
}
});
if (isDestinationUpdated) {
await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
actor
});
}
if (isSourceUpdated) {
await snapshotService.performSnapshot(sourceFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
secretPath: sourceFolder.path,
environmentSlug: sourceFolder.environment.slug,
actorId,
actor
});
}
return {
projectId: project.id,
isSourceUpdated,
isDestinationUpdated
};
};
return {
attachTags,
detachTags,
@ -2105,7 +1703,6 @@ export const secretServiceFactory = ({
updateManySecretsRaw,
deleteManySecretsRaw,
getSecretVersions,
backfillSecretReferences,
moveSecrets
backfillSecretReferences
};
};

View File

@ -397,13 +397,3 @@ export type TSyncSecretsDTO<T extends boolean = false> = {
// used for import creation to trigger replication
pickOnlyImportIds?: string[];
});
export type TMoveSecretsDTO = {
projectSlug: string;
sourceEnvironment: string;
sourceSecretPath: string;
destinationEnvironment: string;
destinationSecretPath: string;
secretIds: string[];
shouldOverwrite: boolean;
} & Omit<TProjectPermission, "projectId">;

View File

@ -75,7 +75,14 @@ access the Infisical API using the AWS Auth authentication method.
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 prompted to configure the authentication method for it. Here, select **AWS Auth**.
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 by default, you should re-configure it to use AWS Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new AWS Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create aws auth method](/images/platform/identities/identities-org-create-aws-auth-method.png)

View File

@ -75,7 +75,14 @@ access the Infisical API using the Azure Auth authentication method.
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 prompted to configure the authentication method for it. Here, select **Azure Auth**.
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 by default, you should re-configure it to use Azure Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new Azure Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create azure auth method](/images/platform/identities/identities-org-create-azure-auth-method.png)

View File

@ -81,7 +81,14 @@ access the Infisical API using the GCP ID Token authentication method.
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 prompted to configure the authentication method for it. Here, select **GCP Auth** and set the **Type** to **GCP ID Token Auth**.
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 by default, you should re-configure it to use GCP Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new GCP Auth configuration onto the identity; set the **Type** field to **GCP ID Token Auth**.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create gcp auth method](/images/platform/identities/identities-org-create-gcp-gce-auth-method.png)
@ -243,9 +250,16 @@ access the Infisical API using the GCP IAM authentication method.
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 prompted to configure the authentication method for it. Here, select **GCP IAM Auth** and set the **Type** to **GCP IAM Auth**.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities create gcp auth method](/images/platform/identities/identities-org-create-gcp-iam-auth-method.png)
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with Universal Auth by default, you should re-configure it to use GCP Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new GCP Auth configuration onto the identity; set the **Type** field to **GCP IAM Auth**.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities organization create token auth method](/images/platform/identities/identities-org-create-gcp-iam-auth-method.png)
Here's some more guidance on each field:

View File

@ -42,9 +42,9 @@ To be more specific:
4. If all is well, Infisical returns a short-lived access token that the application can use to make authenticated requests to the Infisical API.
<Note>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
to authenticate with Infisical using Kubernetes Auth as they handle the
authentication process including service account credential retrieval for you.
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
to authenticate with Infisical using Kubernetes Auth as they handle the
authentication process including service account credential retrieval for you.
</Note>
## Guide
@ -137,9 +137,16 @@ In the following steps, we explore how to create and use identities for your app
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 prompted to configure the authentication method for it. Here, select **Kubernetes Auth**.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png)
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with Universal Auth by default, you should re-configure it to use Kubernetes Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new Kubernetes Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities organization create kubernetes auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png)
Here's some more guidance on each field:
@ -240,8 +247,8 @@ In the following steps, we explore how to create and use identities for your app
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 time and each call to renew it will extend the toke life by increments of access token TTL.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation
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.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation.
</Accordion>
</AccordionGroup>

View File

@ -7,7 +7,7 @@ description: "Learn how to use Machine Identities to programmatically interact w
An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP).
Each identity must authenticate with the Infisical API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth), [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth), [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), or [GCP Auth](/documentation/platform/identities/gcp-auth) to get back a short-lived access token to be used in subsequent requests.
Each identity must authenticate with the Infisical API using a supported authentication method like [Token Auth](/documentation/platform/identities/token-auth), [Universal Auth](/documentation/platform/identities/universal-auth), [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth), [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), or [GCP Auth](/documentation/platform/identities/gcp-auth) to get back a short-lived access token to be used in subsequent requests.
![Organization Identities](/images/platform/organization/organization-machine-identities.png)
@ -28,13 +28,15 @@ A typical workflow for using identities consists of four steps:
## Authentication Methods
To interact with various resources in Infisical, Machine Identities are able to authenticate using:
To interact with various resources in Infisical, Machine Identities can authenticate with the Infisical API using:
- [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method that can be configured on an identity suitable to authenticate from any platform/environment.
- [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth): A Kubernetes-native authentication method for applications (e.g. pods) to authenticate with Infisical.
- [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for AWS services (e.g. EC2, Lambda functions, etc.) to authenticate with Infisical.
- [Azure Auth](/documentation/platform/identities/azure-auth): An Azure-native authentication method for Azure resources (e.g. Azure VMs, Azure App Services, Azure Functions, Azure Kubernetes Service, etc.) to authenticate with Infisical.
- [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.) to authenticate with Infisical.
- [Token Auth](/documentation/platform/identities/token-auth): A platform-agnostic, simple authentication method suitable to authenticate with Infisical using a token.
- [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method suitable to authenticate with Infisical using a Client ID and Client Secret.
- [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth): A Kubernetes-native authentication method for applications (e.g. pods).
- [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for AWS services (e.g. EC2, Lambda functions, etc.).
- [Azure Auth](/documentation/platform/identities/azure-auth): An Azure-native authentication method for Azure resources (e.g. Azure VMs, Azure App Services, Azure Functions, Azure Kubernetes Service, etc.).
- [GCP Auth](/documentation/platform/identities/gcp-auth): A GCP-native authentication method for GCP resources (e.g. Compute Engine, App Engine, Cloud Run, Google Kubernetes Engine, IAM service accounts, etc.).
- [OIDC Auth](/documentation/platform/identities/oidc-auth): A platform-agnostic, JWT-based authentication method for workloads using an OpenID Connect identity provider.
## FAQ
@ -52,6 +54,8 @@ You can learn more about how to do this in the CLI quickstart [here](/cli/usage)
Amongst many differences, identities provide broader access over the Infisical API, utilizes the same
permission system as user identities, and come with a significantly larger number of configurable authentication and security features.
If you're looking for a simple authentication method, similar to service tokens, that can be bound onto an identity, we recommend checking out [Token Auth](/documentation/platform/identities/token-auth).
</Accordion>
<Accordion title="Why can I not create, read, update, or delete an identity?">
There are a few reasons for why this might happen:

View File

@ -0,0 +1,165 @@
---
title: OIDC Auth
description: "Learn how to authenticate with Infisical from any platform or environment using OpenID Connect (OIDC)."
---
**OIDC Auth** is a platform-agnostic JWT-based authentication method that can be used to authenticate from any platform or environment using an identity provider with OpenID Connect.
## Diagram
The following sequence digram illustrates the OIDC Auth workflow for authenticating clients with Infisical.
```mermaid
sequenceDiagram
participant Client as Client
participant Idp as Identity Provider
participant Infis as Infisical
Client->>Idp: Step 1: Request identity token
Idp-->>Client: Return JWT with verifiable claims
Note over Client,Infis: Step 2: Login Operation
Client->>Infis: Send signed JWT to /api/v1/auth/oidc-auth/login
Note over Infis,Idp: Step 3: Query verification
Infis->>Idp: Request JWT public key using OIDC Discovery
Idp-->>Infis: Return public key
Note over Infis: Step 4: JWT validation
Infis->>Client: Return short-lived access token
Note over Client,Infis: Step 5: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the short-lived access token
```
## Concept
At a high-level, Infisical authenticates a client by verifying the JWT and checking that it meets specific requirements (e.g. it is issued by a trusted identity provider) at the `/api/v1/auth/oidc-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 requests an identity token from its identity provider.
2. The client sends the identity token to Infisical at the `/api/v1/auth/oidc-auth/login` endpoint.
3. Infisical fetches the public key that was used to sign the identity token from the identity provider using OIDC Discovery.
4. Infisical validates the JWT using the public key provided by the identity provider and checks that the subject, audience, and claims of the token matches with the set criteria.
5. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
<Note>
Infisical needs network-level access to the identity provider configuration
endpoints.
</Note>
## Guide
In the following steps, we explore how to create and use identities to access the Infisical API using the OIDC Auth authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, 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)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 by default, you should re-configure it to use OIDC Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new OIDC Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities create oidc auth method](/images/platform/identities/identities-org-create-oidc-auth-method.png)
<Tip>Restrict access by configuring the Subject, Audiences, and Claims fields</Tip>
Here's some more guidance on each field:
- OIDC Discovery URL: The URL used to retrieve the OpenID Connect configuration information from the identity provider. This will be used to fetch the public key needed for verifying the provided JWT.
- Issuer: The unique identifier of the identity provider issuing the JWT. This value is used to verify the iss (issuer) claim in the JWT to ensure the token is issued by a trusted provider.
- CA Certificate: The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.
- Subject: The expected principal that is the subject of the JWT. The `sub` (subject) claim in the JWT should match this value.
- Audiences: A list of intended recipients. This value is checked against the aud (audience) claim in the token. The token's aud claim should match at least one of the audiences for it to be valid.
- Claims: Additional information or attributes that should be present in the JWT for it to be valid.
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess 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 acccess 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 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.
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you need to fetch an identity token from an identity provider and make a request to the `/api/v1/auth/oidc-auth/login` endpoint in exchange for an access token.
We provide an example below of how authentication is done with Infisical using OIDC. It is a snippet from the [official Github secrets action](https://github.com/Infisical/secrets-action).
#### Sample usage
```javascript
export const oidcLogin = async ({ identityId, domain, oidcAudience }) => {
const idToken = await core.getIDToken(oidcAudience);
const loginData = querystring.stringify({
identityId,
jwt: idToken,
});
try {
const response = await axios({
method: "post",
url: `${domain}/api/v1/auth/oidc-auth/login`,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: loginData,
});
return response.data.accessToken;
} catch (err) {
core.error("Error:", err.message);
throw err;
}
};
```
#### Sample OIDC login response
```bash Response
{
"accessToken": "...",
"expiresIn": 7200,
"accessTokenMaxTTL": 43244
"tokenType": "Bearer"
}
```
<Tip>
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using OIDC Auth as they handle the authentication process including the fetching of identity tokens for you.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) 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.
</Note>
</Step>
</Steps>

View File

@ -0,0 +1,138 @@
---
title: Token Auth
description: "Learn how to authenticate to Infisical from any platform or environment using an access token."
---
**Token Auth** is a platform-agnostic, simple authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to authenticate from any platform/environment using a token.
## Diagram
The following sequence digram illustrates the Token Auth workflow for authenticating clients with Infisical.
```mermaid
sequenceDiagram
participant Client as Client
participant Infis as Infisical
Note over Client,Infis: Access Infisical API with Token
Client->>Infis: Make authenticated requests using the token
```
## Concept
Token Auth is the simplest authentication method that a client can use to authenticate with Infisical.
Unlike other authentication methods where a client must exchange credential(s) for a short-lived access token to access the Infisical API,
Token Auth allows a client to make authenticated requests to the Infisical API directly using a token. Conceptually, this is similar to using an API Key.
To be more specific:
1. An operator creates an access token in the Infisical UI.
2. The operator shares the access token with the client which it can then use to make authenticated requests to the Infisical API.
## Guide
In the following steps, we explore how to create and use identities for your workloads and applications to access the Infisical API
using the Token Auth authentication method.
<Steps>
<Step title="Creating an identity">
To create an identity, 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)
Now input a few details for your new identity. Here's some guidance for each field:
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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 by default, you should re-configure it to use Token Auth instead. To do this, press to edit the **Authentication** section,
remove the existing Universal Auth configuration, and add a new Token Auth configuration onto the identity.
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
![identities organization create token auth method](/images/platform/identities/identities-org-create-token-auth-method.png)
Here's some more guidance on each field:
- Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess 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 acccess 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 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.
<Warning>
Restricting access token usage to specific trusted IPs is a paid feature.
If youre using Infisical Cloud, then it is available under the Pro Tier. If youre self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Warning>
</Step>
<Step title="Creating a Token">
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)
![identities client secret create](/images/platform/identities/identities-token-auth-create-3.png)
Copy the token and keep it handy as you'll need it to authenticate with the Infisical API.
</Step>
<Step title="Adding an identity to a project">
To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project.
To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to.
![identities project](/images/platform/identities/identities-project.png)
![identities project create](/images/platform/identities/identities-project-create.png)
</Step>
<Step title="Accessing the Infisical API with the identity">
To access the Infisical API as the identity, you can use the generated access token from step 2
to authenticate with the [Infisical API](/api-reference/overview/introduction).
<Note>
Each identity access token has a time-to-live (TLL) 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,
a new access token should be obtained.
</Note>
</Step>
</Steps>
**FAQ**
<AccordionGroup>
<Accordion title="Why is the Infisical API rejecting my access token?">
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 access token is being used from an untrusted IP.
</Accordion>
<Accordion title="What is access token renewal and TTL/Max TTL?">
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.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation.
</Accordion>
</AccordionGroup>

View File

@ -3,7 +3,7 @@ title: Universal Auth
description: "Learn how to authenticate to Infisical from any platform or environment."
---
**Universal Auth** is a platform-agnostic authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) suitable to authenticate from any platform/environment.
**Universal Auth** is a platform-agnostic authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to authenticate from any platform/environment using a Client ID and Client Secret.
## Diagram
@ -55,9 +55,16 @@ using the Universal Auth authentication method.
- Name (required): A friendly name for the identity.
- Role (required): A role from the **Organization 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)
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)
By default, the identity has been configured with Universal Auth. If you wish, you can edit the Universal Auth configuration
details by pressing to edit the **Authentication** section.
![identities organization create universal auth method](/images/platform/identities/identities-org-create-universal-auth-method.png)
Here's some more guidance on each field:
@ -77,12 +84,12 @@ using the Universal Auth authentication method.
<Step title="Creating a Client Secret">
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. With that, press on the key icon on the identity to generate a **Client Secret**
for it.
and password used to authenticate with the Infisical API.
With that, press **Create Client Secret**.
![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)
![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:
@ -131,7 +138,7 @@ using the Universal Auth authentication method.
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
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,
a new access token should be obtained by performing another login operation.
@ -148,7 +155,6 @@ using the Universal Auth authentication method.
- The client secret or access token has expired.
- The identity is insufficently 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.
</Accordion>
<Accordion title="What is access token renewal and TTL/Max TTL?">
@ -156,8 +162,8 @@ using the Universal Auth authentication method.
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 time and each call to renew it will extend the toke life by increments of access token TTL.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation
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.
Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation.
</Accordion>
</AccordionGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 484 KiB

View File

@ -162,11 +162,13 @@
"pages": [
"documentation/platform/auth-methods/email-password",
"documentation/platform/token",
"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/oidc-auth",
"documentation/platform/mfa",
{
"group": "SSO",

View File

@ -1,9 +1,11 @@
import { IdentityAuthMethod } from "./enums";
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.TOKEN_AUTH]: "Token Auth",
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
[IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth",
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth"
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth"
};

View File

@ -1,7 +1,9 @@
export enum IdentityAuthMethod {
TOKEN_AUTH = "token-auth",
UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth"
AZURE_AUTH = "azure-auth",
OIDC_AUTH = "oidc-auth"
}

View File

@ -5,23 +5,42 @@ export {
useAddIdentityAzureAuth,
useAddIdentityGcpAuth,
useAddIdentityKubernetesAuth,
useAddIdentityOidcAuth,
useAddIdentityTokenAuth,
useAddIdentityUniversalAuth,
useCreateIdentity,
useCreateIdentityUniversalAuthClientSecret,
useCreateTokenIdentityTokenAuth,
useDeleteIdentity,
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth,
useDeleteIdentityOidcAuth,
useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret,
useUpdateIdentity,
useUpdateIdentityAwsAuth,
useUpdateIdentityAzureAuth,
useUpdateIdentityGcpAuth,
useUpdateIdentityKubernetesAuth,
useUpdateIdentityUniversalAuth
useUpdateIdentityTokenAuth,
useUpdateIdentityTokenAuthToken,
useUpdateIdentityUniversalAuth,
useUpdateIdentityOidcAuth
} from "./mutations";
export {
useGetIdentityAwsAuth,
useGetIdentityAzureAuth,
useGetIdentityById,
useGetIdentityGcpAuth,
useGetIdentityKubernetesAuth,
useGetIdentityProjectMemberships,
useGetIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
useGetIdentityUniversalAuthClientSecrets,
useGetIdentityOidcAuth
} from "./queries";

View File

@ -9,25 +9,44 @@ import {
AddIdentityAzureAuthDTO,
AddIdentityGcpAuthDTO,
AddIdentityKubernetesAuthDTO,
AddIdentityOidcAuthDTO,
AddIdentityTokenAuthDTO,
AddIdentityUniversalAuthDTO,
ClientSecretData,
CreateIdentityDTO,
CreateIdentityUniversalAuthClientSecretDTO,
CreateIdentityUniversalAuthClientSecretRes,
CreateTokenIdentityTokenAuthDTO,
CreateTokenIdentityTokenAuthRes,
DeleteIdentityAwsAuthDTO,
DeleteIdentityAzureAuthDTO,
DeleteIdentityDTO,
DeleteIdentityGcpAuthDTO,
DeleteIdentityKubernetesAuthDTO,
DeleteIdentityTokenAuthDTO,
DeleteIdentityUniversalAuthClientSecretDTO,
DeleteIdentityUniversalAuthDTO,
DeleteIdentityOidcAuthDTO,
Identity,
IdentityAccessToken,
IdentityAwsAuth,
IdentityAzureAuth,
IdentityGcpAuth,
IdentityKubernetesAuth,
IdentityOidcAuth,
IdentityTokenAuth,
IdentityUniversalAuth,
RevokeTokenDTO,
RevokeTokenRes,
UpdateIdentityAwsAuthDTO,
UpdateIdentityAzureAuthDTO,
UpdateIdentityDTO,
UpdateIdentityGcpAuthDTO,
UpdateIdentityKubernetesAuthDTO,
UpdateIdentityUniversalAuthDTO
UpdateIdentityOidcAuthDTO,
UpdateIdentityUniversalAuthDTO,
UpdateIdentityTokenAuthDTO,
UpdateTokenIdentityTokenAuthDTO
} from "./types";
export const useCreateIdentity = () => {
@ -58,8 +77,9 @@ export const useUpdateIdentity = () => {
return identity;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
}
});
};
@ -103,8 +123,10 @@ export const useAddIdentityUniversalAuth = () => {
});
return identityUniversalAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
}
});
};
@ -131,8 +153,27 @@ export const useUpdateIdentityUniversalAuth = () => {
});
return identityUniversalAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
}
});
};
export const useDeleteIdentityUniversalAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityUniversalAuth, {}, DeleteIdentityUniversalAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityUniversalAuth }
} = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}`);
return identityUniversalAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
}
});
};
@ -214,8 +255,10 @@ export const useAddIdentityGcpAuth = () => {
return identityGcpAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId));
}
});
};
@ -252,8 +295,27 @@ export const useUpdateIdentityGcpAuth = () => {
return identityGcpAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId));
}
});
};
export const useDeleteIdentityGcpAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityGcpAuth, {}, DeleteIdentityGcpAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityGcpAuth }
} = await apiRequest.delete(`/api/v1/auth/gcp-auth/identities/${identityId}`);
return identityGcpAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId));
}
});
};
@ -288,8 +350,10 @@ export const useAddIdentityAwsAuth = () => {
return identityAwsAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId));
}
});
};
@ -324,8 +388,132 @@ export const useUpdateIdentityAwsAuth = () => {
return identityAwsAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId));
}
});
};
export const useDeleteIdentityAwsAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityAwsAuth, {}, DeleteIdentityAwsAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityAwsAuth }
} = await apiRequest.delete(`/api/v1/auth/aws-auth/identities/${identityId}`);
return identityAwsAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId));
}
});
};
export const useUpdateIdentityOidcAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityOidcAuth, {}, UpdateIdentityOidcAuthDTO>({
mutationFn: async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject
}) => {
const {
data: { identityOidcAuth }
} = await apiRequest.patch<{ identityOidcAuth: IdentityOidcAuth }>(
`/api/v1/auth/oidc-auth/identities/${identityId}`,
{
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityOidcAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId));
}
});
};
export const useAddIdentityOidcAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityOidcAuth, {}, AddIdentityOidcAuthDTO>({
mutationFn: async ({
identityId,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityOidcAuth }
} = await apiRequest.post<{ identityOidcAuth: IdentityOidcAuth }>(
`/api/v1/auth/oidc-auth/identities/${identityId}`,
{
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityOidcAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId));
}
});
};
export const useDeleteIdentityOidcAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, DeleteIdentityOidcAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityOidcAuth }
} = await apiRequest.delete(`/api/v1/auth/oidc-auth/identities/${identityId}`);
return identityOidcAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId));
}
});
};
@ -360,8 +548,10 @@ export const useAddIdentityAzureAuth = () => {
return identityAzureAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId));
}
});
};
@ -402,8 +592,10 @@ export const useAddIdentityKubernetesAuth = () => {
return identityKubernetesAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId));
}
});
};
@ -438,8 +630,27 @@ export const useUpdateIdentityAzureAuth = () => {
return identityAzureAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId));
}
});
};
export const useDeleteIdentityAzureAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityAzureAuth, {}, DeleteIdentityAzureAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityAzureAuth }
} = await apiRequest.delete(`/api/v1/auth/azure-auth/identities/${identityId}`);
return identityAzureAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId));
}
});
};
@ -480,8 +691,164 @@ export const useUpdateIdentityKubernetesAuth = () => {
return identityKubernetesAuth;
},
onSuccess: (_, { organizationId }) => {
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId));
}
});
};
export const useDeleteIdentityKubernetesAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, DeleteIdentityKubernetesAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityKubernetesAuth }
} = await apiRequest.delete(`/api/v1/auth/kubernetes-auth/identities/${identityId}`);
return identityKubernetesAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId));
}
});
};
export const useAddIdentityTokenAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, AddIdentityTokenAuthDTO>({
mutationFn: async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityTokenAuth }
} = await apiRequest.post<{ identityTokenAuth: IdentityTokenAuth }>(
`/api/v1/auth/token-auth/identities/${identityId}`,
{
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityTokenAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
}
});
};
export const useUpdateIdentityTokenAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, UpdateIdentityTokenAuthDTO>({
mutationFn: async ({
identityId,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}) => {
const {
data: { identityTokenAuth }
} = await apiRequest.patch<{ identityTokenAuth: IdentityTokenAuth }>(
`/api/v1/auth/token-auth/identities/${identityId}`,
{
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}
);
return identityTokenAuth;
},
onSuccess: (_, { identityId, organizationId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
}
});
};
export const useDeleteIdentityTokenAuth = () => {
const queryClient = useQueryClient();
return useMutation<IdentityTokenAuth, {}, DeleteIdentityTokenAuthDTO>({
mutationFn: async ({ identityId }) => {
const {
data: { identityTokenAuth }
} = await apiRequest.delete(`/api/v1/auth/token-auth/identities/${identityId}`);
return identityTokenAuth;
},
onSuccess: (_, { organizationId, identityId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
queryClient.invalidateQueries(identitiesKeys.getIdentityTokenAuth(identityId));
}
});
};
export const useCreateTokenIdentityTokenAuth = () => {
const queryClient = useQueryClient();
return useMutation<CreateTokenIdentityTokenAuthRes, {}, CreateTokenIdentityTokenAuthDTO>({
mutationFn: async ({ identityId, name }) => {
const { data } = await apiRequest.post<CreateTokenIdentityTokenAuthRes>(
`/api/v1/auth/token-auth/identities/${identityId}/tokens`,
{
name
}
);
return data;
},
onSuccess: (_, { identityId }) => {
queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId));
}
});
};
export const useUpdateIdentityTokenAuthToken = () => {
const queryClient = useQueryClient();
return useMutation<IdentityAccessToken, {}, UpdateTokenIdentityTokenAuthDTO>({
mutationFn: async ({ tokenId, name }) => {
const {
data: { token }
} = await apiRequest.patch<{ token: IdentityAccessToken }>(
`/api/v1/auth/token-auth/tokens/${tokenId}`,
{
name
}
);
return token;
},
onSuccess: (_, { identityId }) => {
queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId));
}
});
};
export const useRevokeIdentityTokenAuthToken = () => {
const queryClient = useQueryClient();
return useMutation<RevokeTokenRes, {}, RevokeTokenDTO>({
mutationFn: async ({ tokenId }) => {
const { data } = await apiRequest.post<RevokeTokenRes>(
`/api/v1/auth/token-auth/tokens/${tokenId}/revoke`
);
return data;
},
onSuccess: (_, { identityId }) => {
queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId));
}
});
};

View File

@ -4,13 +4,19 @@ import { apiRequest } from "@app/config/request";
import {
ClientSecretData,
IdentityAccessToken,
IdentityAwsAuth,
IdentityAzureAuth,
IdentityGcpAuth,
IdentityKubernetesAuth,
IdentityUniversalAuth} from "./types";
IdentityMembershipOrg,
IdentityTokenAuth,
IdentityUniversalAuth,
IdentityOidcAuth
} from "./types";
export const identitiesKeys = {
getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
getIdentityUniversalAuth: (identityId: string) =>
[{ identityId }, "identity-universal-auth"] as const,
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
@ -18,8 +24,42 @@ export const identitiesKeys = {
getIdentityKubernetesAuth: (identityId: string) =>
[{ identityId }, "identity-kubernetes-auth"] as const,
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,
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
getIdentityTokensTokenAuth: (identityId: string) =>
[{ identityId }, "identity-tokens-token-auth"] as const,
getIdentityProjectMemberships: (identityId: string) =>
[{ identityId }, "identity-project-memberships"] as const
};
export const useGetIdentityById = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityById(identityId),
queryFn: async () => {
const {
data: { identity }
} = await apiRequest.get<{ identity: IdentityMembershipOrg }>(
`/api/v1/identities/${identityId}`
);
return identity;
}
});
};
export const useGetIdentityProjectMemberships = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityProjectMemberships(identityId),
queryFn: async () => {
const {
data: { identityMemberships }
} = await apiRequest.get(`/api/v1/identities/${identityId}/identity-memberships`);
return identityMemberships;
}
});
};
export const useGetIdentityUniversalAuth = (identityId: string) => {
@ -33,7 +73,9 @@ export const useGetIdentityUniversalAuth = (identityId: string) => {
`/api/v1/auth/universal-auth/identities/${identityId}`
);
return identityUniversalAuth;
}
},
staleTime: 0,
cacheTime: 0
});
};
@ -63,7 +105,9 @@ export const useGetIdentityGcpAuth = (identityId: string) => {
`/api/v1/auth/gcp-auth/identities/${identityId}`
);
return identityGcpAuth;
}
},
staleTime: 0,
cacheTime: 0
});
};
@ -78,7 +122,9 @@ export const useGetIdentityAwsAuth = (identityId: string) => {
`/api/v1/auth/aws-auth/identities/${identityId}`
);
return identityAwsAuth;
}
},
staleTime: 0,
cacheTime: 0
});
};
@ -93,7 +139,9 @@ export const useGetIdentityAzureAuth = (identityId: string) => {
`/api/v1/auth/azure-auth/identities/${identityId}`
);
return identityAzureAuth;
}
},
staleTime: 0,
cacheTime: 0
});
};
@ -108,6 +156,57 @@ export const useGetIdentityKubernetesAuth = (identityId: string) => {
`/api/v1/auth/kubernetes-auth/identities/${identityId}`
);
return identityKubernetesAuth;
},
staleTime: 0,
cacheTime: 0
});
};
export const useGetIdentityTokenAuth = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityTokenAuth(identityId),
queryFn: async () => {
const {
data: { identityTokenAuth }
} = await apiRequest.get<{ identityTokenAuth: IdentityTokenAuth }>(
`/api/v1/auth/token-auth/identities/${identityId}`
);
return identityTokenAuth;
},
staleTime: 0,
cacheTime: 0
});
};
export const useGetIdentityTokensTokenAuth = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityTokensTokenAuth(identityId),
queryFn: async () => {
const {
data: { tokens }
} = await apiRequest.get<{ tokens: IdentityAccessToken[] }>(
`/api/v1/auth/token-auth/identities/${identityId}/tokens`
);
return tokens;
}
});
};
export const useGetIdentityOidcAuth = (identityId: string) => {
return useQuery({
enabled: Boolean(identityId),
queryKey: identitiesKeys.getIdentityOidcAuth(identityId),
queryFn: async () => {
const {
data: { identityOidcAuth }
} = await apiRequest.get<{ identityOidcAuth: IdentityOidcAuth }>(
`/api/v1/auth/oidc-auth/identities/${identityId}`
);
return identityOidcAuth;
},
staleTime: 0,
cacheTime: 0
});
};

View File

@ -16,6 +16,22 @@ export type Identity = {
updatedAt: string;
};
export type IdentityAccessToken = {
id: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUses: number;
accessTokenNumUsesLimit: number;
accessTokenLastUsedAt: string | null;
accessTokenLastRenewedAt: string | null;
isAccessTokenRevoked: boolean;
identityUAClientSecretId: string | null;
identityId: string;
createdAt: string;
updatedAt: string;
name: string | null;
};
export type IdentityMembershipOrg = {
id: string;
identity: Identity;
@ -113,6 +129,11 @@ export type UpdateIdentityUniversalAuthDTO = {
}[];
};
export type DeleteIdentityUniversalAuthDTO = {
organizationId: string;
identityId: string;
};
export type IdentityGcpAuth = {
identityId: string;
type: "iam" | "gce";
@ -155,6 +176,64 @@ export type UpdateIdentityGcpAuthDTO = {
}[];
};
export type DeleteIdentityGcpAuthDTO = {
organizationId: string;
identityId: string;
};
export type IdentityOidcAuth = {
identityId: string;
oidcDiscoveryUrl: string;
caCert: string;
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityOidcAuthDTO = {
organizationId: string;
identityId: string;
oidcDiscoveryUrl: string;
caCert: string;
boundIssuer: string;
boundAudiences: string;
boundClaims: Record<string, string>;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityOidcAuthDTO = {
organizationId: string;
identityId: string;
oidcDiscoveryUrl?: string;
caCert?: string;
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record<string, string>;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type DeleteIdentityOidcAuthDTO = {
organizationId: string;
identityId: string;
};
export type IdentityAwsAuth = {
identityId: string;
type: "iam";
@ -195,6 +274,11 @@ export type UpdateIdentityAwsAuthDTO = {
}[];
};
export type DeleteIdentityAwsAuthDTO = {
organizationId: string;
identityId: string;
};
export type IdentityAzureAuth = {
identityId: string;
tenantId: string;
@ -234,6 +318,11 @@ export type UpdateIdentityAzureAuthDTO = {
}[];
};
export type DeleteIdentityAzureAuthDTO = {
organizationId: string;
identityId: string;
};
export type IdentityKubernetesAuth = {
identityId: string;
kubernetesHost: string;
@ -282,6 +371,11 @@ export type UpdateIdentityKubernetesAuthDTO = {
}[];
};
export type DeleteIdentityKubernetesAuthDTO = {
organizationId: string;
identityId: string;
};
export type CreateIdentityUniversalAuthClientSecretDTO = {
identityId: string;
description?: string;
@ -311,3 +405,65 @@ export type DeleteIdentityUniversalAuthClientSecretDTO = {
identityId: string;
clientSecretId: string;
};
export type IdentityTokenAuth = {
identityId: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[];
};
export type AddIdentityTokenAuthDTO = {
organizationId: string;
identityId: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: {
ipAddress: string;
}[];
};
export type UpdateIdentityTokenAuthDTO = {
organizationId: string;
identityId: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: {
ipAddress: string;
}[];
};
export type DeleteIdentityTokenAuthDTO = {
organizationId: string;
identityId: string;
};
export type CreateTokenIdentityTokenAuthDTO = {
identityId: string;
name: string;
};
export type CreateTokenIdentityTokenAuthRes = {
accessToken: string;
tokenType: string;
expiresIn: number;
accessTokenMaxTTL: number;
};
export type UpdateTokenIdentityTokenAuthDTO = {
identityId: string;
tokenId: string;
name?: string;
};
export type RevokeTokenDTO = {
identityId: string;
tokenId: string;
};
export type RevokeTokenRes = {
message: string;
};

View File

@ -13,9 +13,15 @@ export const useGetLDAPConfig = (organizationId: string) => {
return useQuery({
queryKey: ldapConfigKeys.getLDAPConfig(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get(`/api/v1/ldap/config?organizationId=${organizationId}`);
try {
const { data } = await apiRequest.get(
`/api/v1/ldap/config?organizationId=${organizationId}`
);
return data;
return data;
} catch (err) {
return null;
}
},
enabled: true
});

View File

@ -12,11 +12,15 @@ export const useGetOIDCConfig = (orgSlug: string) => {
return useQuery({
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
queryFn: async () => {
const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
);
try {
const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
);
return data;
return data;
} catch (err) {
return null;
}
},
enabled: true
});

View File

@ -4,7 +4,6 @@ export {
useCreateSecretV3,
useDeleteSecretBatch,
useDeleteSecretV3,
useMoveSecrets,
useUpdateSecretBatch,
useUpdateSecretV3
} from "./mutations";

View File

@ -17,7 +17,6 @@ import {
TCreateSecretsV3DTO,
TDeleteSecretBatchDTO,
TDeleteSecretsV3DTO,
TMoveSecretsDTO,
TUpdateSecretBatchDTO,
TUpdateSecretsV3DTO
} from "./types";
@ -88,11 +87,11 @@ export const useCreateSecretV3 = ({
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -149,11 +148,11 @@ export const useUpdateSecretV3 = ({
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -245,11 +244,11 @@ export const useCreateSecretBatch = ({
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -298,11 +297,11 @@ export const useUpdateSecretBatch = ({
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
@ -376,73 +375,6 @@ export const useDeleteSecretBatch = ({
});
};
export const useMoveSecrets = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TMoveSecretsDTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient();
return useMutation<
{
isSourceUpdated: boolean;
isDestinationUpdated: boolean;
},
{},
TMoveSecretsDTO
>({
mutationFn: async ({
sourceEnvironment,
sourceSecretPath,
projectSlug,
destinationEnvironment,
destinationSecretPath,
secretIds,
shouldOverwrite
}) => {
const { data } = await apiRequest.post<{
isSourceUpdated: boolean;
isDestinationUpdated: boolean;
}>("/api/v3/secrets/move", {
sourceEnvironment,
sourceSecretPath,
projectSlug,
destinationEnvironment,
destinationSecretPath,
secretIds,
shouldOverwrite
});
return data;
},
onSuccess: (_, { projectId, sourceEnvironment, sourceSecretPath }) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret({
workspaceId: projectId,
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({
environment: sourceEnvironment,
workspaceId: projectId,
directory: sourceSecretPath
})
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({
environment: sourceEnvironment,
workspaceId: projectId,
directory: sourceSecretPath
})
);
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId: projectId }));
},
...options
});
};
export const createSecret = async (dto: CreateSecretDTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data;

View File

@ -177,17 +177,6 @@ export type TDeleteSecretBatchDTO = {
}>;
};
export type TMoveSecretsDTO = {
projectSlug: string;
projectId: string;
sourceEnvironment: string;
sourceSecretPath: string;
destinationEnvironment: string;
destinationSecretPath: string;
secretIds: string[];
shouldOverwrite: boolean;
};
export type CreateSecretDTO = {
workspaceId: string;
environment: string;

View File

@ -11,9 +11,15 @@ export const useGetSSOConfig = (organizationId: string) => {
return useQuery({
queryKey: ssoConfigKeys.getSSOConfig(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get(`/api/v1/sso/config?organizationId=${organizationId}`);
try {
const { data } = await apiRequest.get(
`/api/v1/sso/config?organizationId=${organizationId}`
);
return data;
return data;
} catch (err) {
return null;
}
},
enabled: true
});

View File

@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IdentityPage } from "@app/views/Org/IdentityPage";
export default function Identity() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<IdentityPage />
</>
);
}
Identity.requireAuth = true;

View File

@ -0,0 +1,333 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip,
UpgradePlanModal
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import {
useDeleteIdentity,
useGetIdentityById,
useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
import { IdentityUniversalAuthClientSecretModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal";
import {
IdentityAuthenticationSection,
IdentityClientSecretModal,
IdentityDetailsSection,
IdentityProjectsSection,
IdentityTokenListModal,
IdentityTokenModal
} from "./components";
export const IdentityPage = withPermission(
() => {
const router = useRouter();
const identityId = router.query.identityId as string;
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity",
"deleteIdentity",
"identityAuthMethod",
"revokeAuthMethod",
"token",
"tokenList",
"revokeToken",
"clientSecret",
"revokeClientSecret",
"universalAuthClientSecret", // list of client secrets
"upgradePlan"
] as const);
const onDeleteIdentitySubmit = async (id: string) => {
try {
await deleteIdentity({
identityId: id,
organizationId: orgId
});
createNotification({
text: "Successfully deleted identity",
type: "success"
});
handlePopUpClose("deleteIdentity");
router.push(`/org/${orgId}/members`);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete identity";
createNotification({
text,
type: "error"
});
}
};
const onRevokeTokenSubmit = async ({
identityId: parentIdentityId,
tokenId,
name
}: {
identityId: string;
tokenId: string;
name: string;
}) => {
try {
await revokeToken({
identityId: parentIdentityId,
tokenId
});
handlePopUpClose("revokeToken");
createNotification({
text: `Successfully revoked token ${name ?? ""}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete identity";
createNotification({
text,
type: "error"
});
}
};
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
try {
if (!data?.identity.id) return;
await revokeClientSecret({
identityId: data?.identity.id,
clientSecretId
});
handlePopUpToggle("revokeClientSecret", false);
createNotification({
text: "Successfully deleted client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete client secret",
type: "error"
});
}
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/org/${orgId}/members`);
}}
className="mb-4"
>
Identities
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">{data.identity.name}</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
role: data.role,
customRole: data.customRole
});
}}
disabled={!isAllowed}
>
Edit Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
authMethod: data.identity.authMethod
});
}}
disabled={!isAllowed}
>
{`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("deleteIdentity", {
identityId,
name: data.identity.name
});
}}
disabled={!isAllowed}
>
Delete Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex">
<div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
<IdentityAuthenticationSection
identityId={identityId}
handlePopUpOpen={handlePopUpOpen}
/>
</div>
<IdentityProjectsSection identityId={identityId} />
</div>
</div>
)}
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityAuthMethodModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityTokenListModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteIdentitySubmit(
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
)
}
/>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const revokeTokenData = popUp?.revokeToken?.data as {
identityId: string;
tokenId: string;
name: string;
};
return onRevokeTokenSubmit(revokeTokenData);
}}
/>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })
?.clientSecretPrefix || ""
}************?`}
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
};
return onDeleteClientSecretSubmit({
clientSecretId: deleteClientSecretData.clientSecretId
});
}}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity }
);

View File

@ -0,0 +1,86 @@
import { faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
IconButton,
// Button,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useGetIdentityById } from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityClientSecrets } from "./IdentityClientSecrets";
import { IdentityTokens } from "./IdentityTokens";
type Props = {
identityId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
[
"clientSecret",
"identityAuthMethod",
"revokeClientSecret",
"token",
"revokeToken",
"universalAuthClientSecret",
"tokenList"
]
>,
data?: {}
) => void;
};
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
const { data } = useGetIdentityById(identityId);
return data ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Tooltip content={`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`}>
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
authMethod: data.identity.authMethod
})
}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
</div>
<div className="py-4">
<div className="flex justify-between">
<p className="text-sm font-semibold text-mineshaft-300">Auth Method</p>
</div>
<p className="text-sm text-mineshaft-300">
{data.identity.authMethod
? identityAuthToNameMap[data.identity.authMethod]
: "Not configured"}
</p>
</div>
{data.identity.authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{data.identity.authMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
</div>
) : (
<div />
);
};

View File

@ -0,0 +1,140 @@
import { faCheck, faCopy,faKey, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks";
import {
useGetIdentityById,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
identityId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["clientSecret", "revokeClientSecret", "universalAuthClientSecret"]
>,
data?: {}
) => void;
};
const SHOW_LIMIT = 3;
export const IdentityClientSecrets = ({ identityId, handlePopUpOpen }: Props) => {
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
initialState: "Copy Client ID to clipboard"
});
const { data } = useGetIdentityById(identityId);
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(identityId);
const { data: clientSecrets } = useGetIdentityUniversalAuthClientSecrets(identityId);
return (
<div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Client ID</p>
<div className="flex align-top">
<p className="text-sm text-mineshaft-300">{identityUniversalAuth?.clientId ?? ""}</p>
<Tooltip content={copyTextClientId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "");
setCopyTextClientId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingClientId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
{clientSecrets?.length ? (
<div className="flex justify-between">
<p className="text-sm font-semibold text-mineshaft-300">{`Client Secrets (${clientSecrets.length})`}</p>
<Button
variant="link"
onClick={() => {
handlePopUpOpen("universalAuthClientSecret", {
identityId,
name: data?.identity.name ?? ""
});
}}
>
Manage
</Button>
</div>
) : (
<div />
)}
{clientSecrets
?.slice(0, SHOW_LIMIT)
.map(({ id, clientSecretTTL, clientSecretPrefix, createdAt }) => {
let expiresAt;
if (clientSecretTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
}
return (
<div
className="group flex items-center justify-between py-2 last:pb-0"
key={`client-secret-${id}`}
>
<div className="flex items-center">
<FontAwesomeIcon size="1x" icon={faKey} />
<div className="ml-4">
<p className="text-sm font-semibold text-mineshaft-300">
{`${clientSecretPrefix}****`}
</p>
<p className="text-sm text-mineshaft-300">
{expiresAt ? `Expires on ${format(expiresAt, "yyyy-MM-dd")}` : "No Expiry"}
</p>
</div>
</div>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Revoke Client Secret">
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("revokeClientSecret", {
clientSecretId: id,
clientSecretPrefix
});
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
</div>
);
})}
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Button
isDisabled={!isAllowed}
className="mt-4 w-full"
colorSchema="primary"
type="submit"
onClick={() => {
handlePopUpOpen("clientSecret", {
identityId
});
}}
>
Create Client Secret
</Button>
);
}}
</OrgPermissionCan>
</div>
);
};

View File

@ -0,0 +1,121 @@
import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
identityId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["token", "tokenList", "revokeToken"]>,
data?: {}
) => void;
};
export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
const { data } = useGetIdentityById(identityId);
const { data: tokens } = useGetIdentityTokensTokenAuth(identityId);
return (
<div>
{tokens?.length ? (
<div className="flex justify-between">
<p className="text-sm font-semibold text-mineshaft-300">{`Access Tokens (${tokens.length})`}</p>
<Button
variant="link"
onClick={() => {
handlePopUpOpen("tokenList", {
identityId,
name: data?.identity.name ?? ""
});
}}
>
Manage
</Button>
</div>
) : (
<div />
)}
{tokens?.map((token) => {
const expiresAt = new Date(
new Date(token.createdAt).getTime() + token.accessTokenMaxTTL * 1000
);
return (
<div
className="group flex items-center justify-between py-2 last:pb-0"
key={`identity-token-${token.id}`}
>
<div className="flex items-center">
<FontAwesomeIcon size="1x" icon={faKey} />
<div className="ml-4">
<p className="text-sm font-semibold text-mineshaft-300">
{token.name ? token.name : "-"}
</p>
<p className="text-sm text-mineshaft-300">
{token.isAccessTokenRevoked
? "Revoked"
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="opacity-0 transition-opacity duration-300 hover:text-primary-400 group-hover:opacity-100 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={async () => {
handlePopUpOpen("token", {
identityId,
tokenId: token.id,
name: token.name
});
}}
>
Edit Token
</DropdownMenuItem>
{!token.isAccessTokenRevoked && (
<DropdownMenuItem
onClick={async () => {
handlePopUpOpen("revokeToken", {
identityId,
tokenId: token.id,
name: token.name
});
}}
>
Revoke Token
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
})}
<Button
className="mt-4 mr-4 w-full"
colorSchema="primary"
type="submit"
onClick={() => {
handlePopUpOpen("token", {
identityId
});
}}
>
Create Token
</Button>
</div>
);
};

View File

@ -0,0 +1 @@
export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection";

View File

@ -0,0 +1,191 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { useCreateIdentityUniversalAuthClientSecret } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
description: z.string(),
ttl: z.string(),
numUsesLimit: z.string()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["clientSecret"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret"]>, state?: boolean) => void;
};
export const IdentityClientSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
const { mutateAsync: createClientSecret } = useCreateIdentityUniversalAuthClientSecret();
const [token, setToken] = useState("");
const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const hasToken = Boolean(token);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
description: "",
ttl: "",
numUsesLimit: ""
}
});
const popUpData = popUp?.clientSecret?.data as {
identityId: string;
};
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
try {
const { clientSecret } = await createClientSecret({
identityId: popUpData.identityId,
description,
ttl: Number(ttl),
numUsesLimit: Number(numUsesLimit)
});
setToken(clientSecret);
createNotification({
text: "Successfully created client secret",
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to create client secret";
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.clientSecret?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("clientSecret", isOpen);
reset();
setToken("");
}}
>
<ModalContent
title="Create Client Secret"
subTitle={hasToken ? "We will only show this secret once" : ""}
>
{!hasToken ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Description"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="My Client Secret" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL (seconds - optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<div className="flex">
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</div>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="numUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("clientSecret", false)}
>
Cancel
</Button>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<Tooltip content={copyTextToken}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setCopyTextToken("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingToken ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
)}
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,87 @@
import { faCheck,faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetIdentityById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
identityId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>,
data?: {}
) => void;
};
export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { data } = useGetIdentityById(identityId);
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Details</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Edit Identity">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
role: data.role,
customRole: data.customRole
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Identity ID</p>
<div className="flex align-top">
<p className="text-sm text-mineshaft-300">{data.identity.id}</p>
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(data.identity.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
<p className="text-sm text-mineshaft-300">{data.role}</p>
</div>
</div>
</div>
) : (
<div />
);
};

View File

@ -0,0 +1,57 @@
import { faKey } from "@fortawesome/free-solid-svg-icons";
import {
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useGetIdentityProjectMemberships } from "@app/hooks/api";
type Props = {
identityId: string;
};
export const IdentityProjectsSection = ({ identityId }: Props) => {
const { data: projectMemberships, isLoading } = useGetIdentityProjectMemberships(identityId);
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Projects</h3>
</div>
<div className="py-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} innerKey="identity-project-memberships" />}
{!isLoading &&
projectMemberships?.map((membership: any) => {
// TODO: fix any
return (
<Tr className="h-10" key={`identity-project-membership-${membership.id}`}>
<Td>{membership.project.name}</Td>
<Td>{membership.roles[0].role}</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && !projectMemberships?.length && (
<EmptyState title="This identity has not been assigned to any projects" icon={faKey} />
)}
</TableContainer>
</div>
</div>
);
};

View File

@ -0,0 +1,269 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateTokenIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuthClientSecrets} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
name: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["tokenList", "revokeToken"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["revokeToken"]>, data?: {}) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["tokenList", "revokeToken"]>,
state?: boolean
) => void;
};
export const IdentityTokenListModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
const popUpData = popUp?.tokenList?.data as {
identityId: string;
name: string;
};
const { data: tokens } = useGetIdentityTokensTokenAuth(popUpData?.identityId ?? "");
const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientSecretCopied) {
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientSecretCopied]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientIdCopied) {
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientIdCopied]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!popUpData?.identityId) return;
const newTokenData = await createToken({
identityId: popUpData.identityId,
name
});
setToken(newTokenData.accessToken);
createNotification({
text: "Successfully created token",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create token",
type: "error"
});
}
};
const hasToken = Boolean(token);
return (
<Modal
isOpen={popUp?.tokenList?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("tokenList", isOpen);
reset();
setToken("");
}}
>
<ModalContent title={`Manage Access Tokens for ${popUpData?.name ?? ""}`}>
<h2 className="mb-4">New Token</h2>
{hasToken ? (
<div>
<div className="mb-4 flex items-center justify-between">
<p>We will only show this token once</p>
<Button
colorSchema="secondary"
type="submit"
onClick={() => {
reset();
setToken("");
}}
>
Got it
</Button>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setIsClientSecretCopied.on();
}}
>
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<div className="flex">
<Input {...field} placeholder="My Token" />
<Button
className="ml-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</div>
</FormControl>
)}
/>
</form>
)}
<h2 className="mb-4">Tokens</h2>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>name</Th>
<Th>Num Uses</Th>
<Th>Created At</Th>
<Th>Max Expires At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={5} innerKey="identities-tokens" />}
{!isLoading &&
tokens?.map(
({
id,
createdAt,
name,
accessTokenNumUses,
accessTokenNumUsesLimit,
accessTokenMaxTTL,
isAccessTokenRevoked
}) => {
const expiresAt = new Date(
new Date(createdAt).getTime() + accessTokenMaxTTL * 1000
);
return (
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
<Td>{name === "" ? "-" : name}</Td>
<Td>{`${accessTokenNumUses}${
accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""
}`}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td>
{isAccessTokenRevoked ? "Revoked" : `${format(expiresAt, "yyyy-MM-dd")}`}
</Td>
<Td>
{!isAccessTokenRevoked && (
<IconButton
onClick={() => {
handlePopUpOpen("revokeToken", {
identityId: popUpData?.identityId,
tokenId: id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</Td>
</Tr>
);
}
)}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No tokens have been created for this identity yet"
icon={faKey}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,183 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Tooltip
} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { useCreateTokenIdentityTokenAuth, useUpdateIdentityTokenAuthToken } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
name: z.string()
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["token"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["token"]>, state?: boolean) => void;
};
export const IdentityTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth();
const { mutateAsync: updateToken } = useUpdateIdentityTokenAuthToken();
const [token, setToken] = useState("");
const [copyTextToken, isCopyingToken, setCopyTextToken] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const hasToken = Boolean(token);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: ""
}
});
const tokenData = popUp?.token?.data as {
identityId: string;
tokenId?: string;
name?: string;
};
useEffect(() => {
if (tokenData?.tokenId && tokenData?.name) {
reset({
name: tokenData.name
});
} else {
reset({
name: ""
});
}
}, [popUp?.token?.data]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (tokenData?.tokenId) {
// update
await updateToken({
identityId: tokenData.identityId,
tokenId: tokenData.tokenId,
name
});
handlePopUpToggle("token", false);
} else {
// create
const newTokenData = await createToken({
identityId: tokenData.identityId,
name
});
setToken(newTokenData.accessToken);
// note: may be helpful to tell user ttl etc.
}
createNotification({
text: `Successfully ${popUp?.token?.data ? "updated" : "created"} token`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.token?.data ? "update" : "create"} token`;
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.token?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("token", isOpen);
reset();
setToken("");
}}
>
<ModalContent
title={`${tokenData?.tokenId ? "Update" : "Create"} Access Token`}
subTitle={hasToken ? "We will only show this token once" : ""}
>
{!hasToken ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="My Token" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{tokenData?.name ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("token", false)}
>
Cancel
</Button>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<Tooltip content={copyTextToken}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setCopyTextToken("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingToken ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
)}
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,6 @@
export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/IdentityAuthenticationSection";
export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
export { IdentityDetailsSection } from "./IdentityDetailsSection";
export { IdentityProjectsSection } from "./IdentityProjectsSection";
export { IdentityTokenListModal } from "./IdentityTokenListModal";
export { IdentityTokenModal } from "./IdentityTokenModal";

View File

@ -0,0 +1 @@
export { IdentityPage } from "./IdentityPage";

View File

@ -3,7 +3,9 @@ import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import {
DeleteActionModal,
FormControl,
Modal,
ModalContent,
@ -11,30 +13,44 @@ import {
SelectItem,
UpgradePlanModal
} from "@app/components/v2";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth,
useDeleteIdentityOidcAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
state?: boolean
) => void;
};
const identityAuthMethods = [
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
];
const schema = yup
@ -46,6 +62,17 @@ const schema = yup
export type FormData = yup.InferType<typeof schema>;
export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { control, watch, setValue } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
@ -117,12 +144,109 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
/>
);
}
case IdentityAuthMethod.OIDC_AUTH: {
return (
<IdentityOidcAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
case IdentityAuthMethod.TOKEN_AUTH: {
return (
<IdentityTokenAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
default: {
return <div />;
}
}
};
const onRevokeAuthMethodSubmit = async () => {
if (!identityAuthMethodData.authMethod) return;
if (!orgId) return;
try {
console.log("onRevokeAuthMethodSubmit identityId: ", identityAuthMethodData);
switch (identityAuthMethodData.authMethod) {
case IdentityAuthMethod.UNIVERSAL_AUTH: {
await revokeUniversalAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.TOKEN_AUTH: {
await revokeTokenAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
await revokeKubernetesAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.GCP_AUTH: {
await revokeGcpAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.AWS_AUTH: {
await revokeAwsAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.AZURE_AUTH: {
await revokeAzureAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.OIDC_AUTH: {
await revokeOidcAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
default:
break;
}
createNotification({
text: `Successfully removed ${
identityAuthToNameMap[identityAuthMethodData.authMethod]
} on ${identityAuthMethodData.name}`,
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to remove ${identityAuthToNameMap[identityAuthMethodData.authMethod]} on ${
identityAuthMethodData.name
}`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.identityAuthMethod?.isOpen}
@ -163,6 +287,17 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
onDeleteApproved={onRevokeAuthMethodSubmit}
/>
</ModalContent>
</Modal>
);

View File

@ -42,7 +42,7 @@ export type FormData = yup.InferType<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
@ -329,23 +329,36 @@ export const IdentityAwsAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);

View File

@ -40,7 +40,7 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
@ -327,23 +327,36 @@ export const IdentityAzureAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);

View File

@ -41,7 +41,7 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
@ -361,23 +361,36 @@ export const IdentityGcpAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);

View File

@ -17,8 +17,6 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
// TODO: Add CA cert and token reviewer JWT fields
const schema = z
.object({
kubernetesHost: z.string(),
@ -45,7 +43,7 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
@ -77,11 +75,11 @@ export const IdentityKubernetesAuthForm = ({
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
kubernetesHost: "", // TODO
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "", // TODO
allowedNamespaces: "", // TODO
allowedAudience: "", // TODO
allowedNames: "",
allowedNamespaces: "",
allowedAudience: "",
caCert: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
@ -118,7 +116,7 @@ export const IdentityKubernetesAuthForm = ({
});
} else {
reset({
kubernetesHost: "", // TODO
kubernetesHost: "",
tokenReviewerJwt: "",
allowedNames: "",
allowedNamespaces: "",
@ -384,23 +382,36 @@ export const IdentityKubernetesAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);

View File

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@ -16,8 +17,8 @@ import {
import { useOrganization } from "@app/context";
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import {
IdentityAuthMethod
// useAddIdentityUniversalAuth
// IdentityAuthMethod,
useAddIdentityUniversalAuth
} from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
@ -32,18 +33,19 @@ export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["identity"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
data: {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
}
) => void;
// handlePopUpOpen: (
// popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
// data: {
// identityId: string;
// name: string;
// authMethod?: IdentityAuthMethod;
// }
// ) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
};
export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
const router = useRouter();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -51,7 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
const { mutateAsync: createMutateAsync } = useCreateIdentity();
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
// const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const {
control,
@ -113,32 +115,30 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
} else {
// create
const {
id: createdId,
name: createdName,
authMethod
} = await createMutateAsync({
const { id: createdId } = await createMutateAsync({
name,
role: role || undefined,
organizationId: orgId
});
// await addMutateAsync({
// organizationId: orgId,
// identityId: createdId,
// clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
// accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
// accessTokenTTL: 2592000,
// accessTokenMaxTTL: 2592000,
// accessTokenNumUsesLimit: 0
// });
await addMutateAsync({
organizationId: orgId,
identityId: createdId,
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0
});
handlePopUpToggle("identity", false);
handlePopUpOpen("identityAuthMethod", {
identityId: createdId,
name: createdName,
authMethod
});
router.push(`/org/${orgId}/identities/${createdId}`);
// handlePopUpOpen("identityAuthMethod", {
// identityId: createdId,
// name: createdName,
// authMethod
// });
}
createNotification({

View File

@ -0,0 +1,483 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
accessTokenTrustedIps: z
.array(
z.object({
ipAddress: z.string().max(50)
})
)
.min(1),
accessTokenTTL: z.string(),
accessTokenMaxTTL: z.string(),
accessTokenNumUsesLimit: z.string(),
oidcDiscoveryUrl: z.string().url().min(1),
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: z.string().optional().default(""),
boundClaims: z.array(
z.object({
key: z.string(),
value: z.string()
})
),
boundSubject: z.string().optional().default("")
});
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
};
};
export const IdentityOidcAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "");
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
}
});
const {
fields: boundClaimsFields,
append: appendBoundClaimField,
remove: removeBoundClaimField
} = useFieldArray({
control,
name: "boundClaims"
});
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
remove: removeAccessTokenTrustedIp
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
useEffect(() => {
if (data) {
reset({
oidcDiscoveryUrl: data.oidcDiscoveryUrl,
caCert: data.caCert,
boundIssuer: data.boundIssuer,
boundAudiences: data.boundAudiences,
boundClaims: Object.entries(data.boundClaims).map(([key, value]) => ({
key,
value
})),
boundSubject: data.boundSubject,
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({
oidcDiscoveryUrl: "",
caCert: "",
boundIssuer: "",
boundAudiences: "",
boundClaims: [],
boundSubject: "",
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({
accessTokenTrustedIps,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims,
boundSubject
}: FormData) => {
try {
if (!identityAuthMethodData) {
return;
}
if (data) {
await updateMutateAsync({
identityId: identityAuthMethodData.identityId,
organizationId: orgId,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
boundSubject,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
} else {
await addMutateAsync({
identityId: identityAuthMethodData.identityId,
oidcDiscoveryUrl,
caCert,
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
boundSubject,
organizationId: orgId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
}
handlePopUpToggle("identityAuthMethod", false);
createNotification({
text: `Successfully ${
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success"
});
reset();
} catch (err) {
createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="oidcDiscoveryUrl"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="OIDC Discovery URL"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="https://token.actions.githubusercontent.com"
type="text"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="boundIssuer"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Issuer"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="text"
placeholder="https://token.actions.githubusercontent.com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => (
<FormControl label="Subject" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="boundAudiences"
render={({ field, fieldState: { error } }) => (
<FormControl label="Audiences" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} type="text" placeholder="service1, service2" />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`boundClaims.${index}.key`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Claims" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="property"
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name={`boundClaims.${index}.value`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="value1, value2"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => removeBoundClaimField(index)}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() =>
appendBoundClaimField({
key: "",
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Claims
</Button>
</div>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -15,10 +15,11 @@ import { withPermission } from "@app/hoc";
import { useDeleteIdentity } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal";
// import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal";
import { IdentityModal } from "./IdentityModal";
import { IdentityTable } from "./IdentityTable";
import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
// import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
export const IdentitySection = withPermission(
() => {
@ -33,7 +34,8 @@ export const IdentitySection = withPermission(
"deleteIdentity",
"universalAuthClientSecret",
"deleteUniversalAuthClientSecret",
"upgradePlan"
"upgradePlan",
"tokenAuthToken"
] as const);
const isMoreIdentitiesAllowed = subscription?.identityLimit
@ -106,22 +108,19 @@ export const IdentitySection = withPermission(
)}
</OrgPermissionCan>
</div>
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
<IdentityModal
<IdentityTable />
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
{/* <IdentityAuthMethodModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityAuthMethodModal
/> */}
{/* <IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
/> */}
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${

View File

@ -1,22 +1,11 @@
import {
faCopy,
faEllipsis,
faKey,
faLock,
faPencil,
faServer,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import Link from "next/link";
import { useRouter } from "next/router";
import { faEllipsis, faServer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Select,
@ -28,33 +17,13 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["deleteIdentity", "identity", "universalAuthClientSecret", "identityAuthMethod"]
>,
data?: {
identityId?: string;
name?: string;
authMethod?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
}
) => void;
};
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
export const IdentityTable = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -94,19 +63,22 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th>Auth Method</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ identity: { id, name, authMethod }, role, customRole }) => {
data?.map(({ identity: { id, name }, role, customRole }) => {
return (
<Tr className="h-10" key={`identity-${id}`}>
<Td>{name}</Td>
<Tr
className="h-10 cursor-pointer transition-colors duration-300 hover:bg-mineshaft-700"
key={`identity-${id}`}
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
>
<Td>
<Link href={`/org/${orgId}/identities/${id}`}>{name}</Link>
</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
@ -136,124 +108,16 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
}}
</OrgPermissionCan>
</Td>
<Td>{authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}</Td>
<Td>
<div className="flex items-center justify-end space-x-4">
{authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<Tooltip content="Manage client ID/secrets">
<IconButton
onClick={async () => {
handlePopUpOpen("universalAuthClientSecret", {
identityId: id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faKey} />
</IconButton>
</Tooltip>
)}
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
>
{(isAllowed) => (
<Tooltip content="Manage auth method">
<IconButton
onClick={async () => {
handlePopUpOpen("identityAuthMethod", {
identityId: id,
name,
authMethod
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faLock} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
if (!isAllowed) return;
handlePopUpOpen("identity", {
identityId: id,
name,
role,
customRole
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Update identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {
if (!isAllowed) return;
handlePopUpOpen("deleteIdentity", {
identityId: id,
name
});
}}
icon={<FontAwesomeIcon icon={faXmark} />}
>
Delete identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(id);
createNotification({
text: "Copied identity internal ID to clipboard",
type: "success"
});
}}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Copy Identity ID
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<FontAwesomeIcon icon={faEllipsis} />
</IconButton>
</div>
</Td>
</Tr>

View File

@ -0,0 +1,275 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityTokenAuth,
useGetIdentityTokenAuth,
useUpdateIdentityTokenAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z
.object({
accessTokenTTL: z.string(),
accessTokenMaxTTL: z.string(),
accessTokenNumUsesLimit: z.string(),
accessTokenTrustedIps: z
.array(
z.object({
ipAddress: z.string().max(50)
})
)
.min(1)
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
};
};
export const IdentityTokenAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "");
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
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" });
const onFormSubmit = async ({
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (data) {
await updateMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
} else {
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
}
handlePopUpToggle("identityAuthMethod", false);
createNotification({
text: `Successfully ${
identityAuthMethodData?.authMethod ? "updated" : "configured"
} auth method`,
type: "success"
});
reset();
} catch (err) {
createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -0,0 +1,53 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, Modal, ModalContent, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["tokenAuthToken"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["tokenAuthToken"]>, state?: boolean) => void;
};
export const IdentityTokenAuthTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
const [copyTextAccessToken, isCopyingAccessToken, setCopyTextAccessToken] = useTimedReset<string>(
{
initialState: "Copy to clipboard"
}
);
const popUpData = popUp?.tokenAuthToken?.data as {
accessToken: string;
};
return (
<Modal
isOpen={popUp?.tokenAuthToken?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("tokenAuthToken", isOpen);
}}
>
<ModalContent title="Access Token">
{popUpData?.accessToken && (
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{popUpData.accessToken}</p>
<Tooltip content={copyTextAccessToken}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(popUpData.accessToken);
setCopyTextAccessToken("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingAccessToken ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
)}
</ModalContent>
</Modal>
);
};

View File

@ -10,7 +10,7 @@ import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import {
Button,
DeleteActionModal,
// DeleteActionModal,
EmptyState,
FormControl,
IconButton,
@ -30,8 +30,7 @@ import { useToggle } from "@app/hooks";
import {
useCreateIdentityUniversalAuthClientSecret,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets,
useRevokeIdentityUniversalAuthClientSecret
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@ -44,18 +43,16 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>;
popUp: UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteUniversalAuthClientSecret"]>,
popUpName: keyof UsePopUpState<["revokeClientSecret"]>,
data?: {
clientSecretPrefix: string;
clientSecretId: string;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<
["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]
>,
popUpName: keyof UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>,
state?: boolean
) => void;
};
@ -66,7 +63,7 @@ export const IdentityUniversalAuthClientSecretModal = ({
handlePopUpToggle
}: Props) => {
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
@ -81,8 +78,6 @@ export const IdentityUniversalAuthClientSecretModal = ({
const { mutateAsync: createClientSecretMutateAsync } =
useCreateIdentityUniversalAuthClientSecret();
const { mutateAsync: revokeClientSecretMutateAsync } =
useRevokeIdentityUniversalAuthClientSecret();
const {
control,
@ -142,41 +137,6 @@ export const IdentityUniversalAuthClientSecretModal = ({
}
};
const onDeleteClientSecretSubmit = async ({
clientSecretId,
clientSecretPrefix
}: {
clientSecretId: string;
clientSecretPrefix: string;
}) => {
try {
if (!popUpData?.identityId) return;
await revokeClientSecretMutateAsync({
identityId: popUpData.identityId,
clientSecretId
});
if (token.startsWith(clientSecretPrefix)) {
reset();
setToken("");
}
handlePopUpToggle("deleteUniversalAuthClientSecret", false);
createNotification({
text: "Successfully deleted client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete client secret",
type: "error"
});
}
};
const hasToken = Boolean(token);
return (
@ -346,7 +306,7 @@ export const IdentityUniversalAuthClientSecretModal = ({
<Td>
<IconButton
onClick={() => {
handlePopUpOpen("deleteUniversalAuthClientSecret", {
handlePopUpOpen("revokeClientSecret", {
clientSecretPrefix,
clientSecretId: id
});
@ -376,26 +336,6 @@ export const IdentityUniversalAuthClientSecretModal = ({
</TBody>
</Table>
</TableContainer>
<DeleteActionModal
isOpen={popUp.deleteUniversalAuthClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.deleteUniversalAuthClientSecret?.data as { clientSecretPrefix: string })
?.clientSecretPrefix || ""
}************?`}
onChange={(isOpen) => handlePopUpToggle("deleteUniversalAuthClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = popUp?.deleteUniversalAuthClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
};
return onDeleteClientSecretSubmit({
clientSecretId: deleteClientSecretData.clientSecretId,
clientSecretPrefix: deleteClientSecretData.clientSecretPrefix
});
}}
/>
</ModalContent>
</Modal>
);

View File

@ -48,7 +48,7 @@ export type FormData = yup.InferType<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
@ -368,23 +368,36 @@ export const IdentityUniversalAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
</Button>
<div className="flex justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{identityAuthMethodData?.authMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);

View File

@ -1,9 +1,7 @@
import { useState } from "react";
import { TypeOptions } from "react-toastify";
import { subject } from "@casl/ability";
import {
faAngleDown,
faAnglesRight,
faCheckCircle,
faChevronRight,
faCodeCommit,
@ -48,12 +46,7 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
import { interpolateSecrets } from "@app/helpers/secret";
import { usePopUp } from "@app/hooks";
import {
useCreateFolder,
useDeleteSecretBatch,
useGetUserWsKey,
useMoveSecrets
} from "@app/hooks/api";
import { useCreateFolder, useDeleteSecretBatch, useGetUserWsKey } from "@app/hooks/api";
import { DecryptedSecret, SecretType, TImportedSecrets, WsTag } from "@app/hooks/api/types";
import { debounce } from "@app/lib/fn/debounce";
@ -67,7 +60,6 @@ import { Filter, GroupBy } from "../../SecretMainPage.types";
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
import { CreateSecretImportForm } from "./CreateSecretImportForm";
import { FolderForm } from "./FolderForm";
import { MoveSecretsModal } from "./MoveSecretsModal";
type Props = {
secrets?: DecryptedSecret[];
@ -113,7 +105,6 @@ export const ActionBar = ({
"addDynamicSecret",
"addSecretImport",
"bulkDeleteSecrets",
"moveSecrets",
"misc",
"upgradePlan"
] as const);
@ -123,7 +114,6 @@ export const ActionBar = ({
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
const { mutateAsync: moveSecrets } = useMoveSecrets();
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const selectedSecrets = useSelectedSecrets();
@ -238,55 +228,6 @@ export const ActionBar = ({
}
};
const handleSecretsMove = async ({
destinationEnvironment,
destinationSecretPath,
shouldOverwrite
}: {
destinationEnvironment: string;
destinationSecretPath: string;
shouldOverwrite: boolean;
}) => {
try {
const secretsToMove = secrets.filter(({ id }) => Boolean(selectedSecrets?.[id]));
const { isDestinationUpdated, isSourceUpdated } = await moveSecrets({
projectSlug,
shouldOverwrite,
sourceEnvironment: environment,
sourceSecretPath: secretPath,
destinationEnvironment,
destinationSecretPath,
projectId: workspaceId,
secretIds: secretsToMove.map((sec) => sec.id)
});
let notificationMessage = "";
let notificationType: TypeOptions = "info";
if (isDestinationUpdated && isSourceUpdated) {
notificationMessage = "Successfully moved selected secrets";
notificationType = "success";
} else if (isDestinationUpdated) {
notificationMessage =
"Successfully created secrets in destination. A secret approval request has been generated for the source.";
} else if (isSourceUpdated) {
notificationMessage = "A secret approval request has been generated in the destination";
} else {
notificationMessage =
"A secret approval request has been generated in both the source and the destination.";
}
createNotification({
type: notificationType,
text: notificationMessage
});
resetSelectedSecret();
} catch (error) {
console.error(error);
}
};
return (
<>
<div className="mt-4 flex items-center space-x-2">
@ -514,25 +455,6 @@ export const ActionBar = ({
<div className="ml-4 flex-grow px-2 text-sm">
{Object.keys(selectedSecrets).length} Selected
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
renderTooltip
allowedLabel="Move"
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faAnglesRight} />}
className="ml-4"
onClick={() => handlePopUpOpen("moveSecrets")}
isDisabled={!isAllowed}
size="xs"
>
Move
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@ -544,7 +466,7 @@ export const ActionBar = ({
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
className="ml-2"
className="ml-4"
onClick={() => handlePopUpOpen("bulkDeleteSecrets")}
isDisabled={!isAllowed}
size="xs"
@ -587,11 +509,6 @@ export const ActionBar = ({
onChange={(isOpen) => handlePopUpToggle("bulkDeleteSecrets", isOpen)}
onDeleteApproved={handleSecretBulkDelete}
/>
<MoveSecretsModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
onMoveApproved={handleSecretsMove}
/>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}

View File

@ -1,147 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
Checkbox,
FormControl,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["moveSecrets"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["moveSecrets"]>, state?: boolean) => void;
onMoveApproved: (moveParams: {
destinationEnvironment: string;
destinationSecretPath: string;
shouldOverwrite: boolean;
}) => void;
};
const formSchema = z.object({
environment: z.string().trim(),
secretPath: z
.string()
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
),
shouldOverwrite: z.boolean().default(false)
});
type TFormSchema = z.infer<typeof formSchema>;
export const MoveSecretsModal = ({ popUp, handlePopUpToggle, onMoveApproved }: Props) => {
const {
handleSubmit,
control,
reset,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({ resolver: zodResolver(formSchema) });
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
const selectedEnvironment = watch("environment");
const handleFormSubmit = (data: TFormSchema) => {
onMoveApproved({
destinationEnvironment: data.environment,
destinationSecretPath: data.secretPath,
shouldOverwrite: data.shouldOverwrite
});
handlePopUpToggle("moveSecrets", false);
};
return (
<Modal
isOpen={popUp.moveSecrets.isOpen}
onOpenChange={(isOpen) => {
reset();
handlePopUpToggle("moveSecrets", isOpen);
}}
>
<ModalContent
title="Move Secrets"
subTitle="Move secrets from the current path to the selected destination"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<Controller
control={control}
name="environment"
defaultValue={environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<SecretPathInput {...field} environment={selectedEnvironment} />
</FormControl>
)}
/>
<Controller
control={control}
name="shouldOverwrite"
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<Checkbox
id="overwrite-checkbox"
className="ml-2"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
>
Overwrite existing secrets
</Checkbox>
)}
/>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="move-secrets-submit"
className="mr-4"
type="submit"
>
Move
</Button>
<Button
key="move-secrets-cancel"
onClick={() => handlePopUpToggle("moveSecrets", false)}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};

Some files were not shown because too many files have changed in this diff Show More