mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
17 Commits
gcp-iam-au
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
389d51fa5c | |||
638208e9fa | |||
c176d1e4f7 | |||
91a23a608e | |||
c6a25271dd | |||
0f5c1340d3 | |||
ecbdae110d | |||
8ef727b4ec | |||
c6f24dbb5e | |||
18c0d2fd6f | |||
c1fb8f47bf | |||
990eddeb32 | |||
ce01f8d099 | |||
faf6708b00 | |||
a58d6ebdac | |||
818b136836 | |||
0cdade6a2d |
backend
docs
frontend/src
components
hooks/api
pages/org/[id]
styles
views
Org/MembersPage/components/OrgIdentityTab/components/IdentitySection
SecretMainPage/components/SecretListView
204
backend/package-lock.json
generated
204
backend/package-lock.json
generated
@ -39,8 +39,6 @@
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"google-auth-library": "^9.9.0",
|
||||
"googleapis": "^137.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
@ -6185,14 +6183,6 @@
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
|
||||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
@ -7769,11 +7759,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"node_modules/extsprintf": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
||||
@ -8301,88 +8286,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.5.0.tgz",
|
||||
"integrity": "sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.6.9",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/agent-base": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
|
||||
"integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/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/gcp-metadata": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
|
||||
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
|
||||
"dependencies": {
|
||||
"gaxios": "^6.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
@ -8579,69 +8482,6 @@
|
||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.9.0.tgz",
|
||||
"integrity": "sha512-9l+zO07h1tDJdIHN74SpnWIlNR+OuOemXlWJlLP9pXy6vFtizgpEzMuwJa4lqY9UAdiAv5DVd5ql0Am916I+aA==",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^6.1.1",
|
||||
"gcp-metadata": "^6.1.0",
|
||||
"gtoken": "^7.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/googleapis": {
|
||||
"version": "137.1.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
|
||||
"integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^9.0.0",
|
||||
"googleapis-common": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/googleapis-common": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
|
||||
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"gaxios": "^6.0.3",
|
||||
"google-auth-library": "^9.7.0",
|
||||
"qs": "^6.7.0",
|
||||
"url-template": "^2.0.8",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
@ -8664,37 +8504,6 @@
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gtoken": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
||||
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
||||
"dependencies": {
|
||||
"gaxios": "^6.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken/node_modules/jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken/node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
@ -9468,14 +9277,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
@ -13902,11 +13703,6 @@
|
||||
"querystring": "0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-template": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
|
||||
},
|
||||
"node_modules/url/node_modules/punycode": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
|
@ -100,8 +100,6 @@
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"google-auth-library": "^9.9.0",
|
||||
"googleapis": "^137.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.3.2",
|
||||
"jmespath": "^0.16.0",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -33,7 +33,6 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
@ -117,7 +116,6 @@ declare module "fastify" {
|
||||
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||
identityProject: TIdentityProjectServiceFactory;
|
||||
identityUa: TIdentityUaServiceFactory;
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@ -62,9 +62,6 @@ import {
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
TIdentityAwsAuthsUpdate,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityGcpAuthsInsert,
|
||||
TIdentityGcpAuthsUpdate,
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityOrgMembershipsInsert,
|
||||
TIdentityOrgMembershipsUpdate,
|
||||
@ -332,11 +329,6 @@ declare module "knex/types/tables" {
|
||||
TIdentityUniversalAuthsInsert,
|
||||
TIdentityUniversalAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityGcpAuth]: Knex.CompositeTableType<
|
||||
TIdentityGcpAuths,
|
||||
TIdentityGcpAuthsInsert,
|
||||
TIdentityGcpAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAwsAuth]: Knex.CompositeTableType<
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
|
@ -1,30 +0,0 @@
|
||||
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.IdentityGcpAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityGcpAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
t.string("type").notNullable();
|
||||
t.string("allowedServiceAccounts").notNullable();
|
||||
t.string("allowedProjects").notNullable();
|
||||
t.string("allowedZones").notNullable(); // GCE only (fully qualified zone names)
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityGcpAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityGcpAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityGcpAuth);
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
// 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 IdentityGcpAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
accessTokenTTL: z.coerce.number().default(7200),
|
||||
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
allowedServiceAccounts: z.string(),
|
||||
allowedProjects: z.string(),
|
||||
allowedZones: z.string()
|
||||
});
|
||||
|
||||
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
|
||||
export type TIdentityGcpAuthsInsert = Omit<z.input<typeof IdentityGcpAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityGcpAuthsUpdate = Partial<Omit<z.input<typeof IdentityGcpAuthsSchema>, TImmutableDBKeys>>;
|
@ -18,7 +18,6 @@ export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identity-access-tokens";
|
||||
export * from "./identity-aws-auths";
|
||||
export * from "./identity-gcp-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
|
@ -44,7 +44,6 @@ export enum TableName {
|
||||
Identity = "identities",
|
||||
IdentityAccessToken = "identity_access_tokens",
|
||||
IdentityUniversalAuth = "identity_universal_auths",
|
||||
IdentityGcpAuth = "identity_gcp_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
@ -145,6 +144,5 @@ export enum ProjectUpgradeStatus {
|
||||
|
||||
export enum IdentityAuthMethod {
|
||||
Univeral = "universal-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth"
|
||||
}
|
||||
|
@ -66,10 +66,6 @@ export enum EventType {
|
||||
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",
|
||||
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
|
||||
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
|
||||
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
|
||||
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
|
||||
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
|
||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||
@ -414,52 +410,6 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityGcpAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_GCP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityGcpAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityGcpAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_GCP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
type: string;
|
||||
allowedServiceAccounts: string;
|
||||
allowedProjects: string;
|
||||
allowedZones: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityGcpAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_GCP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
type?: string;
|
||||
allowedServiceAccounts?: string;
|
||||
allowedProjects?: string;
|
||||
allowedZones?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityGcpAuthEvent {
|
||||
type: EventType.GET_IDENTITY_GCP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityAwsAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_AWS_AUTH;
|
||||
metadata: {
|
||||
@ -758,10 +708,6 @@ export type Event =
|
||||
| CreateIdentityUniversalAuthClientSecretEvent
|
||||
| GetIdentityUniversalAuthClientSecretsEvent
|
||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||
| LoginIdentityGcpAuthEvent
|
||||
| AddIdentityGcpAuthEvent
|
||||
| UpdateIdentityGcpAuthEvent
|
||||
| GetIdentityGcpAuthEvent
|
||||
| LoginIdentityAwsAuthEvent
|
||||
| AddIdentityAwsAuthEvent
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
|
@ -90,15 +90,17 @@ export const secretScanningServiceFactory = ({
|
||||
const {
|
||||
data: { repositories }
|
||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||
await Promise.all(
|
||||
repositories.map(({ id, full_name }) =>
|
||||
secretScanningQueue.startFullRepoScan({
|
||||
organizationId: session.orgId,
|
||||
installationId,
|
||||
repository: { id, fullName: full_name }
|
||||
})
|
||||
)
|
||||
);
|
||||
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||
await Promise.all(
|
||||
repositories.map(({ id, full_name }) =>
|
||||
secretScanningQueue.startFullRepoScan({
|
||||
organizationId: session.orgId,
|
||||
installationId,
|
||||
repository: { id, fullName: full_name }
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
return { installatedApp };
|
||||
};
|
||||
|
||||
@ -151,6 +153,7 @@ export const secretScanningServiceFactory = ({
|
||||
};
|
||||
|
||||
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
|
||||
const appCfg = getConfig();
|
||||
const { commits, repository, installation, pusher } = payload;
|
||||
if (!commits || !repository || !installation || !pusher) {
|
||||
return;
|
||||
@ -161,13 +164,15 @@ export const secretScanningServiceFactory = ({
|
||||
});
|
||||
if (!installationLink) return;
|
||||
|
||||
await secretScanningQueue.startPushEventScan({
|
||||
commits,
|
||||
pusher: { name: pusher.name, email: pusher.email },
|
||||
repository: { fullName: repository.full_name, id: repository.id },
|
||||
organizationId: installationLink.orgId,
|
||||
installationId: String(installation?.id)
|
||||
});
|
||||
if (!appCfg.DISABLE_SECRET_SCANNING) {
|
||||
await secretScanningQueue.startPushEventScan({
|
||||
commits,
|
||||
pusher: { name: pusher.name, email: pusher.email },
|
||||
repository: { fullName: repository.full_name, id: repository.id },
|
||||
organizationId: installationLink.orgId,
|
||||
installationId: String(installation?.id)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => {
|
||||
|
@ -13,6 +13,10 @@ const zodStrBool = z
|
||||
const envSchema = z
|
||||
.object({
|
||||
PORT: z.coerce.number().default(4000),
|
||||
DISABLE_SECRET_SCANNING: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((el) => el === "true"),
|
||||
REDIS_URL: zpStr(z.string()),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(
|
||||
|
@ -5,8 +5,13 @@ import { getConfig } from "@app/lib/config/env";
|
||||
export const maintenanceMode = fp(async (fastify) => {
|
||||
fastify.addHook("onRequest", async (req) => {
|
||||
const serverEnvs = getConfig();
|
||||
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) {
|
||||
throw new Error("Infisical is in maintenance mode. Please try again later.");
|
||||
if (serverEnvs.MAINTENANCE_MODE) {
|
||||
// skip if its universal auth login or renew
|
||||
if (req.url === "/api/v1/auth/universal-auth/login" && req.method === "POST") return;
|
||||
if (req.url === "/api/v1/auth/token/renew" && req.method === "POST") return;
|
||||
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET") {
|
||||
throw new Error("Infisical is in maintenance mode. Please try again later.");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -80,8 +80,6 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
|
||||
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
|
||||
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
|
||||
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-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";
|
||||
@ -207,8 +205,6 @@ export const registerRoutes = async (
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(db);
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
const trustedIpDAL = trustedIpDALFactory(db);
|
||||
@ -708,15 +704,6 @@ export const registerRoutes = async (
|
||||
identityUaDAL,
|
||||
licenseService
|
||||
});
|
||||
const identityGcpAuthService = identityGcpAuthServiceFactory({
|
||||
identityGcpAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
@ -794,7 +781,6 @@ export const registerRoutes = async (
|
||||
identityAccessToken: identityAccessTokenService,
|
||||
identityProject: identityProjectService,
|
||||
identityUa: identityUaService,
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
secretApprovalPolicy: sapService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
|
@ -20,16 +20,23 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).merge(
|
||||
z.object({ isMigrationModeOn: z.boolean() })
|
||||
)
|
||||
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
|
||||
isMigrationModeOn: z.boolean(),
|
||||
isSecretScanningDisabled: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async () => {
|
||||
const config = await getServerCfg();
|
||||
const serverEnvs = getConfig();
|
||||
return { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE } };
|
||||
return {
|
||||
config: {
|
||||
...config,
|
||||
isMigrationModeOn: serverEnvs.MAINTENANCE_MODE,
|
||||
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,268 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityGcpAuthsSchema } 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";
|
||||
import { validateGcpAuthField } from "@app/services/identity-gcp-auth/identity-gcp-auth-validators";
|
||||
|
||||
export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/gcp-auth/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Login with GCP Auth",
|
||||
body: z.object({
|
||||
identityId: z.string(),
|
||||
jwt: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
await server.services.identityGcpAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg?.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_GCP_AUTH,
|
||||
metadata: {
|
||||
identityId: identityGcpAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityGcpAuthId: identityGcpAuth.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityGcpAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/gcp-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Attach GCP Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
type: z.enum(["iam", "gce"]),
|
||||
allowedServiceAccounts: validateGcpAuthField,
|
||||
allowedProjects: validateGcpAuthField,
|
||||
allowedZones: validateGcpAuthField,
|
||||
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({
|
||||
identityGcpAuth: IdentityGcpAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityGcpAuth = await server.services.identityGcpAuth.attachGcpAuth({
|
||||
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: identityGcpAuth.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_GCP_AUTH,
|
||||
metadata: {
|
||||
identityId: identityGcpAuth.identityId,
|
||||
type: identityGcpAuth.type,
|
||||
allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts,
|
||||
allowedProjects: identityGcpAuth.allowedProjects,
|
||||
allowedZones: identityGcpAuth.allowedZones,
|
||||
accessTokenTTL: identityGcpAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityGcpAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/gcp-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update GCP Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
type: z.enum(["iam", "gce"]).optional(),
|
||||
allowedServiceAccounts: validateGcpAuthField,
|
||||
allowedProjects: validateGcpAuthField,
|
||||
allowedZones: validateGcpAuthField,
|
||||
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({
|
||||
identityGcpAuth: IdentityGcpAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityGcpAuth = await server.services.identityGcpAuth.updateGcpAuth({
|
||||
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: identityGcpAuth.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_GCP_AUTH,
|
||||
metadata: {
|
||||
identityId: identityGcpAuth.identityId,
|
||||
type: identityGcpAuth.type,
|
||||
allowedServiceAccounts: identityGcpAuth.allowedServiceAccounts,
|
||||
allowedProjects: identityGcpAuth.allowedProjects,
|
||||
allowedZones: identityGcpAuth.allowedZones,
|
||||
accessTokenTTL: identityGcpAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityGcpAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityGcpAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/gcp-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Retrieve GCP Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityGcpAuth: IdentityGcpAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityGcpAuth = await server.services.identityGcpAuth.getGcpAuth({
|
||||
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: identityGcpAuth.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_GCP_AUTH,
|
||||
metadata: {
|
||||
identityId: identityGcpAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityGcpAuth };
|
||||
}
|
||||
});
|
||||
};
|
@ -3,7 +3,6 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityUaRouter } from "./identity-ua";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
@ -29,7 +28,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
async (authRouter) => {
|
||||
await authRouter.register(registerAuthRoutes);
|
||||
await authRouter.register(registerIdentityUaRouter);
|
||||
await authRouter.register(registerIdentityGcpAuthRouter);
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
},
|
||||
|
@ -143,8 +143,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
|
||||
}),
|
||||
body: z.object({
|
||||
app: z.string().trim().describe(INTEGRATION.UPDATE.app),
|
||||
appId: z.string().trim().describe(INTEGRATION.UPDATE.appId),
|
||||
app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
|
||||
appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
|
||||
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
|
||||
secretPath: z
|
||||
.string()
|
||||
@ -154,7 +154,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
.describe(INTEGRATION.UPDATE.secretPath),
|
||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment)
|
||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
||||
metadata: z
|
||||
.object({
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityGcpAuthDALFactory = ReturnType<typeof identityGcpAuthDALFactory>;
|
||||
|
||||
export const identityGcpAuthDALFactory = (db: TDbClient) => {
|
||||
const gcpAuthOrm = ormify(db, TableName.IdentityGcpAuth);
|
||||
return gcpAuthOrm;
|
||||
};
|
@ -1,70 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { OAuth2Client } from "google-auth-library";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TDecodedGcpIamAuthJwt, TGcpIdTokenPayload } from "./identity-gcp-auth-types";
|
||||
|
||||
/**
|
||||
* Validates that the identity token [jwt] sent in from a client GCE instance as part of GCP ID Token authentication
|
||||
* is valid.
|
||||
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
|
||||
* @param {string} jwt - The identity token to validate.
|
||||
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
|
||||
*/
|
||||
export const validateIdTokenIdentity = async ({
|
||||
identityId,
|
||||
jwt: identityToken
|
||||
}: {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
}) => {
|
||||
const oAuth2Client = new OAuth2Client();
|
||||
const response = await oAuth2Client.getFederatedSignonCerts();
|
||||
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
|
||||
identityToken,
|
||||
response.certs,
|
||||
identityId, // audience
|
||||
["https://accounts.google.com"]
|
||||
);
|
||||
const payload = ticket.getPayload() as TGcpIdTokenPayload;
|
||||
if (!payload || !payload.email) throw new UnauthorizedError();
|
||||
|
||||
return { email: payload.email, computeEngineDetails: payload.google?.compute_engine };
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the signed JWT token for a GCP service account is valid as part of GCP IAM authentication.
|
||||
* @param {string} identityId - The ID of the identity in Infisical that is being authenticated against (used as audience).
|
||||
* @param {string} jwt - The signed JWT token to validate.
|
||||
* @param {string} credentials - The credentials in the GCP Auth configuration for Infisical.
|
||||
* @returns
|
||||
*/
|
||||
export const validateIamIdentity = async ({
|
||||
identityId,
|
||||
jwt: serviceAccountJwt
|
||||
}: {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
}) => {
|
||||
const decodedJwt = jwt.decode(serviceAccountJwt, { complete: true }) as TDecodedGcpIamAuthJwt;
|
||||
const { sub, aud } = decodedJwt.payload;
|
||||
|
||||
const {
|
||||
data
|
||||
}: {
|
||||
data: {
|
||||
[key: string]: string;
|
||||
};
|
||||
} = await axios.get(`https://www.googleapis.com/service_accounts/v1/metadata/x509/${sub}`);
|
||||
|
||||
const publicKey = data[decodedJwt.header.kid];
|
||||
|
||||
jwt.verify(serviceAccountJwt, publicKey, {
|
||||
algorithms: ["RS256"]
|
||||
});
|
||||
|
||||
if (aud !== identityId) throw new UnauthorizedError();
|
||||
return { email: sub };
|
||||
};
|
@ -1,324 +0,0 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } 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 { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { 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 { TIdentityGcpAuthDALFactory } from "./identity-gcp-auth-dal";
|
||||
import { validateIamIdentity, validateIdTokenIdentity } from "./identity-gcp-auth-fns";
|
||||
import {
|
||||
TAttachGcpAuthDTO,
|
||||
TGcpIdentityDetails,
|
||||
TGetGcpAuthDTO,
|
||||
TLoginGcpAuthDTO,
|
||||
TUpdateGcpAuthDTO
|
||||
} from "./identity-gcp-auth-types";
|
||||
|
||||
type TIdentityGcpAuthServiceFactoryDep = {
|
||||
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TIdentityGcpAuthServiceFactory = ReturnType<typeof identityGcpAuthServiceFactory>;
|
||||
|
||||
export const identityGcpAuthServiceFactory = ({
|
||||
identityGcpAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TIdentityGcpAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
|
||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||
if (!identityGcpAuth) throw new UnauthorizedError();
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityGcpAuth.identityId });
|
||||
if (!identityMembershipOrg) throw new UnauthorizedError();
|
||||
|
||||
let gcpIdentityDetails: TGcpIdentityDetails;
|
||||
switch (identityGcpAuth.type) {
|
||||
case "gce": {
|
||||
gcpIdentityDetails = await validateIdTokenIdentity({
|
||||
identityId,
|
||||
jwt: gcpJwt
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "iam": {
|
||||
gcpIdentityDetails = await validateIamIdentity({
|
||||
identityId,
|
||||
jwt: gcpJwt
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new BadRequestError({ message: "Invalid GCP Auth type" });
|
||||
}
|
||||
}
|
||||
|
||||
if (identityGcpAuth.allowedServiceAccounts) {
|
||||
// validate if the service account is in the list of allowed service accounts
|
||||
|
||||
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
|
||||
.split(",")
|
||||
.map((serviceAccount) => serviceAccount.trim())
|
||||
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
|
||||
|
||||
if (!isServiceAccountAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
|
||||
// validate if the project that the service account belongs to is in the list of allowed projects
|
||||
|
||||
const isProjectAllowed = identityGcpAuth.allowedProjects
|
||||
.split(",")
|
||||
.map((project) => project.trim())
|
||||
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
|
||||
|
||||
if (!isProjectAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
|
||||
const isZoneAllowed = identityGcpAuth.allowedZones
|
||||
.split(",")
|
||||
.map((zone) => zone.trim())
|
||||
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
|
||||
|
||||
if (!isZoneAllowed) throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityGcpAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityGcpAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityGcpAuth.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, identityGcpAuth, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachGcpAuth = async ({
|
||||
identityId,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TAttachGcpAuthDTO) => {
|
||||
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 GCP 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 identityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityGcpAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
await identityDAL.updateById(
|
||||
identityMembershipOrg.identityId,
|
||||
{
|
||||
authMethod: IdentityAuthMethod.GCP_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateGcpAuth = async ({
|
||||
identityId,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateGcpAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update GCP Auth"
|
||||
});
|
||||
|
||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityGcpAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityGcpAuth.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 updatedGcpAuth = await identityGcpAuthDAL.updateById(identityGcpAuth.id, {
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedGcpAuth,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
};
|
||||
};
|
||||
|
||||
const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have GCP Auth attached"
|
||||
});
|
||||
|
||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachGcpAuth,
|
||||
updateGcpAuth,
|
||||
getGcpAuth
|
||||
};
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginGcpAuthDTO = {
|
||||
identityId: string;
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
export type TAttachGcpAuthDTO = {
|
||||
identityId: string;
|
||||
type: "iam" | "gce";
|
||||
allowedServiceAccounts: string;
|
||||
allowedProjects: string;
|
||||
allowedZones: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateGcpAuthDTO = {
|
||||
identityId: string;
|
||||
type?: "iam" | "gce";
|
||||
allowedServiceAccounts?: string;
|
||||
allowedProjects?: string;
|
||||
allowedZones?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetGcpAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
type TComputeEngineDetails = {
|
||||
instance_creation_timestamp: number;
|
||||
instance_id: string;
|
||||
instance_name: string;
|
||||
project_id: string;
|
||||
project_number: number;
|
||||
zone: string;
|
||||
};
|
||||
|
||||
export type TGcpIdentityDetails = {
|
||||
email: string;
|
||||
computeEngineDetails?: TComputeEngineDetails;
|
||||
};
|
||||
|
||||
export type TGcpIdTokenPayload = {
|
||||
aud: string;
|
||||
azp: string;
|
||||
email: string;
|
||||
email_verified: boolean;
|
||||
exp: number;
|
||||
google?: {
|
||||
compute_engine: TComputeEngineDetails;
|
||||
};
|
||||
iat: number;
|
||||
iss: string;
|
||||
sub: string;
|
||||
};
|
||||
|
||||
export type TDecodedGcpIamAuthJwt = {
|
||||
header: {
|
||||
alg: string;
|
||||
kid: string;
|
||||
typ: string;
|
||||
};
|
||||
payload: {
|
||||
sub: string;
|
||||
aud: string;
|
||||
};
|
||||
signature: string;
|
||||
metadata: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const validateGcpAuthField = z
|
||||
.string()
|
||||
.trim()
|
||||
.default("")
|
||||
.transform((data) => {
|
||||
if (data === "") return "";
|
||||
// Trim each ID and join with ', ' to ensure formatting
|
||||
return data
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.join(", ");
|
||||
});
|
@ -9,9 +9,12 @@
|
||||
|
||||
import {
|
||||
CreateSecretCommand,
|
||||
DescribeSecretCommand,
|
||||
GetSecretValueCommand,
|
||||
ResourceNotFoundException,
|
||||
SecretsManagerClient,
|
||||
TagResourceCommand,
|
||||
UntagResourceCommand,
|
||||
UpdateSecretCommand
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
@ -574,6 +577,7 @@ const syncSecretsAWSSecretManager = async ({
|
||||
if (awsSecretManagerSecret?.SecretString) {
|
||||
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||
}
|
||||
|
||||
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
|
||||
await secretsManager.send(
|
||||
new UpdateSecretCommand({
|
||||
@ -582,7 +586,88 @@ const syncSecretsAWSSecretManager = async ({
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
|
||||
|
||||
if (secretAWSTag && secretAWSTag.length) {
|
||||
const describedSecret = await secretsManager.send(
|
||||
// requires secretsmanager:DescribeSecret policy
|
||||
new DescribeSecretCommand({
|
||||
SecretId: integration.app as string
|
||||
})
|
||||
);
|
||||
|
||||
if (!describedSecret.Tags) return;
|
||||
|
||||
const integrationTagObj = secretAWSTag.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.key] = item.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
const awsTagObj = (describedSecret.Tags || []).reduce(
|
||||
(acc, item) => {
|
||||
if (item.Key && item.Value) {
|
||||
acc[item.Key] = item.Value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
const tagsToUpdate: { Key: string; Value: string }[] = [];
|
||||
const tagsToDelete: { Key: string; Value: string }[] = [];
|
||||
|
||||
describedSecret.Tags?.forEach((tag) => {
|
||||
if (tag.Key && tag.Value) {
|
||||
if (!(tag.Key in integrationTagObj)) {
|
||||
// delete tag from AWS secret manager
|
||||
tagsToDelete.push({
|
||||
Key: tag.Key,
|
||||
Value: tag.Value
|
||||
});
|
||||
} else if (tag.Value !== integrationTagObj[tag.Key]) {
|
||||
// update tag in AWS secret manager
|
||||
tagsToUpdate.push({
|
||||
Key: tag.Key,
|
||||
Value: integrationTagObj[tag.Key]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
secretAWSTag?.forEach((tag) => {
|
||||
if (!(tag.key in awsTagObj)) {
|
||||
// create tag in AWS secret manager
|
||||
tagsToUpdate.push({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (tagsToUpdate.length) {
|
||||
await secretsManager.send(
|
||||
new TagResourceCommand({
|
||||
SecretId: integration.app as string,
|
||||
Tags: tagsToUpdate
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (tagsToDelete.length) {
|
||||
await secretsManager.send(
|
||||
new UntagResourceCommand({
|
||||
SecretId: integration.app as string,
|
||||
TagKeys: tagsToDelete.map((tag) => tag.Key)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// case when AWS manager can't find the specified secret
|
||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||
await secretsManager.send(
|
||||
new CreateSecretCommand({
|
||||
|
@ -103,7 +103,8 @@ export const integrationServiceFactory = ({
|
||||
owner,
|
||||
isActive,
|
||||
environment,
|
||||
secretPath
|
||||
secretPath,
|
||||
metadata
|
||||
}: TUpdateIntegrationDTO) => {
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||
@ -127,7 +128,17 @@ export const integrationServiceFactory = ({
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner,
|
||||
secretPath
|
||||
secretPath,
|
||||
metadata: {
|
||||
...(integration.metadata as object),
|
||||
...metadata
|
||||
}
|
||||
});
|
||||
|
||||
await secretQueueService.syncIntegrations({
|
||||
environment: folder.environment.slug,
|
||||
secretPath,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
|
||||
return updatedIntegration;
|
||||
|
@ -33,13 +33,27 @@ export type TCreateIntegrationDTO = {
|
||||
|
||||
export type TUpdateIntegrationDTO = {
|
||||
id: string;
|
||||
app: string;
|
||||
appId: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
isActive?: boolean;
|
||||
secretPath: string;
|
||||
targetEnvironment: string;
|
||||
owner: string;
|
||||
environment: string;
|
||||
metadata?: {
|
||||
secretPrefix?: string;
|
||||
secretSuffix?: string;
|
||||
secretGCPLabel?: {
|
||||
labelName: string;
|
||||
labelValue: string;
|
||||
};
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteIntegrationDTO = {
|
||||
|
@ -27,7 +27,7 @@ sequenceDiagram
|
||||
Note over Infis: Step 4: Identity Property Validation
|
||||
Infis->>Client: Return short-lived access token
|
||||
|
||||
Note over Client,Infis: Step 5: Access Infisical API with Token
|
||||
Note over Client,Infis: Step 4: Access Infisical API with Token
|
||||
Client->>Infis: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
@ -36,13 +36,9 @@ sequenceDiagram
|
||||
At a high-level, Infisical authenticates an IAM principal by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed IAM principal ARN) at the `/api/v1/auth/aws-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 IAM principal signs a `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html); this is done using the credentials from the AWS environment where the IAM principal is running.
|
||||
2. The client sends the signed query data to Infisical including the request method, request body, and request headers.
|
||||
3. Infisical reconstructs the query and sends it to AWS STS API via the [sts:GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) method for verification and obtains the identity associated with the IAM principal.
|
||||
4. Infisical checks the identity's properties against set criteria such **Allowed Principal ARNs**.
|
||||
5. If all is well, Infisical returns a short-lived access token that the IAM principal can use to make authenticated requests to the Infisical API.
|
||||
In AWS Auth, an IAM principal signs a `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html); this is done using the credentials from the AWS environment where the IAM principal is running.
|
||||
The query data including the request method, request body, and request headers are sent to Infisical afterwhich Infisical forwards the signed query to AWS STS API via the [sts:GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) method to verify and obtain the identity of the IAM principal.
|
||||
Once obtained, the identity information is verified against specified requirements such as if the associated IAM principal ARN is allowed to authenticate with Infisical. If all is well, Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
|
||||
|
||||
<Note>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
|
||||
@ -55,7 +51,7 @@ as part of the AWS Auth workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
## Guide
|
||||
## Workflow
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications on AWS to
|
||||
access the Infisical API using the AWS Auth authentication method.
|
||||
|
@ -1,351 +0,0 @@
|
||||
---
|
||||
title: GCP Auth
|
||||
description: "Learn how to authenticate with Infisical for services on Google Cloud Platform"
|
||||
---
|
||||
|
||||
**GCP Auth** is a GCP-native authentication method for GCP resources to access Infisical. It consists of two sub-methods/approaches:
|
||||
|
||||
- GCP ID Token Auth: For GCP services including [Compute Engine](https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature), [App Engine standard environment](https://cloud.google.com/appengine/docs/standard/python3/runtime#metadata_server), [App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/python/runtime#metadata_server), [Cloud Functions](https://cloud.google.com/functions/docs/securing/function-identity#using_the_metadata_server_to_acquire_tokens), [Cloud Run](https://cloud.google.com/run/docs/container-contract#metadata-server), [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity#instance_metadata), and [Cloud Build](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity#instance_metadata) to authenticate with Infisical.
|
||||
- GCP IAM Auth: For Google Cloud Platform (GCP) service accounts to authenticate with Infisical.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Google ID Token Auth">
|
||||
|
||||
## Diagram
|
||||
|
||||
The following sequence digram illustrates the GCP ID Token Auth workflow for authenticating GCP resources with Infisical.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GCE as GCP Service
|
||||
participant Infis as Infisical
|
||||
participant Google as OAuth2 API
|
||||
|
||||
Note over GCE,Google: Step 1: Instance Identity Token Retrieval
|
||||
GCE->>Google: Request instance identity metadata token
|
||||
Google-->>GCE: Return JWT token with RS256 signature
|
||||
|
||||
Note over GCE,Infis: Step 2: Identity Token Login Operation
|
||||
GCE->>Infis: Send JWT token to /api/v1/auth/gcp-auth/login
|
||||
Infis->>Google: Request OAuth2 certificates
|
||||
Google-->>Infis: Return certificates
|
||||
|
||||
Note over Infis: Step 3: Identity Token Verification
|
||||
Note over Infis: Step 4: Identity Property Validation
|
||||
Infis->>GCE: Return short-lived access token
|
||||
|
||||
Note over GCE,Infis: Step 4: Access Infisical API with Token
|
||||
GCE->>Infis: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## Concept
|
||||
|
||||
At a high-level, Infisical authenticates a GCP resource by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed GCE instance) at the `/api/v1/auth/gcp-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 running on a GCP service obtains an [ID token](https://cloud.google.com/docs/authentication/get-id-token) constituting the identity for a GCP resource such as a GCE instance or Cloud Function; this is a unique JWT token that includes details about the instance as well as Google's [RS256 signature](https://datatracker.ietf.org/doc/html/rfc7518#section-3.3).
|
||||
2. The client sends the ID token to Infisical.
|
||||
3. Infisical verifies the token against Google's [public OAuth2 certificates](https://www.googleapis.com/oauth2/v3/certs).
|
||||
4. Infisical checks if the entity behind the ID token is allowed to authenticate with Infisical based on set criteria such as **Allowed Service Account Emails**.
|
||||
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>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
|
||||
to authenticate with Infisical using GCP ID Token Auth as they handle the
|
||||
authentication process including generating the instance ID token for you.
|
||||
|
||||
Also, note that Infisical needs network-level access to send requests to the Google Cloud API
|
||||
as part of the GCP Auth workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications on GCP to
|
||||
access the Infisical API using the GCP ID Token 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**.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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 prompted to configure the authentication method for it. Here, select **GCP Auth** and set the **Type** to **GCP ID Token Auth**.
|
||||
|
||||

|
||||
|
||||
Here's some more guidance on each field:
|
||||
|
||||
- Allowed Service Account Emails: A comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical; this could be something like `test@project.iam.gserviceaccount.com`, `12345-compute@developer.gserviceaccount.com`, etc.
|
||||
- Allowed Projects: A comma-separated list of trusted GCP projects that the GCE instance must belong to authenticate with Infisical. Note that this validation property will only work for GCE instances.
|
||||
- Allowed Zones: A comma-separated list of trusted zones that the GCE instances must belong to authenticate with Infisical; this should be the fully-qualified zone name in the format `<region>-<zone>`like `us-central1-a`, `us-west1-b`, etc. Note that this validation property will only work for GCE instances.
|
||||
- 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.
|
||||
|
||||

|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Accessing the Infisical API with the identity">
|
||||
To access the Infisical API as the identity, you need to generate an [ID token](https://cloud.google.com/docs/authentication/get-id-token) constituting the identity of the present GCE instance and make a request to the `/api/v1/auth/gcp-auth/login` endpoint containing the token in exchange for an access token.
|
||||
|
||||
We provide a few code examples below of how you can authenticate with Infisical to access the [Infisical API](/api-reference/overview/introduction).
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion
|
||||
title="Sample code for generating the ID token"
|
||||
>
|
||||
Start by making a request from the GCE instance to obtain the ID token.
|
||||
For more examples of how to obtain the token in Java, Go, Node.js, etc. refer to the [official documentation](https://cloud.google.com/docs/authentication/get-id-token#curl).
|
||||
|
||||
#### Sample request
|
||||
<CodeGroup>
|
||||
```bash curl
|
||||
curl -H "Metadata-Flavor: Google" \
|
||||
'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=<identityId>'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
Note that you should replace `<identityId>` with the ID of the identity you created in step 1.
|
||||
</Note>
|
||||
|
||||
Next use send the obtained JWT token along to authenticate with Infisical and obtain an access token.
|
||||
|
||||
#### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/auth/gcp-auth/login' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'identityId=...' \
|
||||
--data-urlencode 'jwt=...'
|
||||
```
|
||||
|
||||
#### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"accessToken": "...",
|
||||
"expiresIn": 7200,
|
||||
"accessTokenMaxTTL": 43244
|
||||
"tokenType": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
Next, you can use the access token to access the [Infisical API](/api-reference/overview/introduction)
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Tip>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token.
|
||||
</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>
|
||||
|
||||
</Tab>
|
||||
<Tab title="GCP IAM Auth">
|
||||
|
||||
## Diagram
|
||||
|
||||
The following sequence digram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GCE as Client
|
||||
participant Infis as Infisical
|
||||
participant Google as Cloud IAM
|
||||
|
||||
Note over GCE,Google: Step 1: Signed JWT Token Generation
|
||||
GCE->>Google: Request to generate signed JWT token
|
||||
Google-->>GCE: Return signed JWT token
|
||||
|
||||
Note over GCE,Infis: Step 2: JWT Token Login Operation
|
||||
GCE->>Infis: Send signed JWT token to /api/v1/auth/gcp-auth/login
|
||||
Infis->>Google: Request public key
|
||||
Google-->>Infis: Return public key
|
||||
|
||||
Note over Infis: Step 3: JWT Token Verification
|
||||
Note over Infis: Step 4: JWT Property Validation
|
||||
Infis->>GCE: Return short-lived access token
|
||||
|
||||
Note over GCE,Infis: Step 5: Access Infisical API with Token
|
||||
GCE->>Infis: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## Concept
|
||||
|
||||
At a high-level, Infisical authenticates an IAM service account by verifying its identity and checking that it meets specific requirements (e.g. it is an allowed service account) at the `/api/v1/auth/gcp-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 generates a signed JWT token using the `projects.serviceAccounts.signJwt` [API method](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt); this is done using the service account credentials associated with the client.
|
||||
2. The client sends the signed JWT token to Infisical.
|
||||
3. Infisical verifies the signed JWT token.
|
||||
4. Infisical checks if the service account behind the JWT token is allowed to authenticate with Infisical based **Allowed Service Account Emails**.
|
||||
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>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent
|
||||
to authenticate with Infisical using GCP IAM Auth as they handle the
|
||||
authentication process including generating the signed JWT token.
|
||||
|
||||
Also, note that Infisical needs network-level access to send requests to the Google Cloud API
|
||||
as part of the GCP Auth workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications on GCP to
|
||||
access the Infisical API using the GCP IAM 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**.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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 prompted to configure the authentication method for it. Here, select **GCP IAM Auth** and set the **Type** to **GCP IAM Auth**.
|
||||
|
||||

|
||||
|
||||
Here's some more guidance on each field:
|
||||
|
||||
- Allowed Service Account Emails: A comma-separated list of trusted IAM service account emails that are allowed to authenticate with Infisical; this could be something like `test@project.iam.gserviceaccount.com`, `12345-compute@developer.gserviceaccount.com`, etc.
|
||||
- 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.
|
||||
|
||||

|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Accessing the Infisical API with the identity">
|
||||
To access the Infisical API as the identity, you need to generate a signed JWT token using the `projects.serviceAccounts.signJwt` [API method](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt) and make a request to the `/api/v1/auth/gcp-auth/login` endpoint containing the signed JWT token in exchange for an access token.
|
||||
|
||||
<Info>
|
||||
Make sure that the service account has the `iam.serviceAccounts.signJwt` permission or the `roles/iam.serviceAccountTokenCreator` role.
|
||||
</Info>
|
||||
|
||||
We provide a few code examples below of how you can authenticate with Infisical to access the [Infisical API](/api-reference/overview/introduction).
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion
|
||||
title="Sample code for generating a signed JWT token"
|
||||
>
|
||||
The following code provides a generic example of how you can generate a signed JWT token against the `projects.serviceAccounts.signJwt` API method.
|
||||
|
||||
The shown example uses Node.js and the official [google-auth-library](https://github.com/googleapis/google-auth-library-nodejs#readme) package but you can use any language you wish.
|
||||
|
||||
|
||||
```javascript
|
||||
const { GoogleAuth } = require("google-auth-library");
|
||||
|
||||
const auth = new GoogleAuth({
|
||||
scopes: "https://www.googleapis.com/auth/cloud-platform",
|
||||
});
|
||||
|
||||
const credentials = await auth.getCredentials();
|
||||
|
||||
const identityId = "<your-infisical-identity-id>";
|
||||
|
||||
const jwtPayload = {
|
||||
sub: credentials.client_email,
|
||||
aud: identityId,
|
||||
};
|
||||
|
||||
const { data } = await client.request({
|
||||
url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${credentials.client_email}:signJwt`,
|
||||
method: "POST",
|
||||
data: { payload: JSON.stringify(jwtPayload) },
|
||||
});
|
||||
|
||||
const jwt = data.signedJwt // send this jwt to Infisical in the next step
|
||||
```
|
||||
|
||||
#### Sample request
|
||||
|
||||
```bash Request
|
||||
curl --location --request POST 'https://app.infisical.com/api/v1/auth/gcp-auth/login' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'identityId=...' \
|
||||
--data-urlencode 'jwt=...'
|
||||
```
|
||||
|
||||
#### Sample response
|
||||
|
||||
```bash Response
|
||||
{
|
||||
"accessToken": "...",
|
||||
"expiresIn": 7200,
|
||||
"accessTokenMaxTTL": 43244
|
||||
"tokenType": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
Next, you can use the access token to access the [Infisical API](/api-reference/overview/introduction)
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Tip>
|
||||
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token.
|
||||
</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>
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
@ -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), [AWS Auth](/documentation/platform/identities/aws-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 using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS Auth](/documentation/platform/identities/aws-auth) to get back a short-lived access token to be used in subsequent requests.
|
||||
|
||||

|
||||
|
||||
@ -21,7 +21,7 @@ Key Features:
|
||||
A typical workflow for using identities consists of four steps:
|
||||
|
||||
1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities.
|
||||
This step also involves configuring an authentication method for it.
|
||||
This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS Auth](/documentation/platform/identities/aws-auth).
|
||||
2. Adding the identity to the project(s) you want it to have access to.
|
||||
3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back.
|
||||
4. Authenticating subsequent requests with the Infisical API using the short-lived access token.
|
||||
@ -39,9 +39,6 @@ To interact with various resources in Infisical, Machine Identities are able to
|
||||
|
||||
- [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.
|
||||
- [AWS Auth](/documentation/platform/identities/aws-auth): An AWS-native authentication method for IAM principals like EC2 instances or Lambda functions 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.
|
||||
|
||||
IAM service accounts and GCE instances to authenticate with Infisical.
|
||||
|
||||
## FAQ
|
||||
|
||||
|
@ -26,14 +26,10 @@ sequenceDiagram
|
||||
|
||||
## Concept
|
||||
|
||||
In this method, Infisical authenticates a client by verifying the credentials issued for it at the `/api/v1/auth/universal-auth/login` endpoint. If successful,
|
||||
In this method, Infisical authenticates an identity by verifying the credentials issued for it at the `/api/v1/auth/universal-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 submits a **Client ID** and **Client Secret** to Infisical.
|
||||
2. Infisical verifies the credential pair.
|
||||
3. If all is well, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
|
||||
In Universal Auth, an identity is given a **Client ID** and one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for a short-lived access token to authenticate with the Infisical API.
|
||||
|
||||
## Guide
|
||||
|
||||
@ -152,7 +148,7 @@ using the Universal Auth authentication method.
|
||||
- The client secret/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.
|
||||
A identity access token can have a time-to-live (TTL) or incremental lifetime afterwhich 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.
|
||||
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 529 KiB |
Binary file not shown.
Before ![]() (image error) Size: 517 KiB |
@ -29,7 +29,9 @@ Prerequisites:
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:CreateSecret",
|
||||
"secretsmanager:UpdateSecret",
|
||||
"secretsmanager:DescribeSecret", // if you need to add tags to secrets
|
||||
"secretsmanager:TagResource", // if you need to add tags to secrets
|
||||
"secretsmanager:UntagResource", // if you need to add tags to secrets
|
||||
"kms:ListKeys", // if you need to specify the KMS key
|
||||
"kms:ListAliases", // if you need to specify the KMS key
|
||||
"kms:Encrypt", // if you need to specify the KMS key
|
||||
|
@ -153,7 +153,6 @@
|
||||
"documentation/platform/auth-methods/email-password",
|
||||
"documentation/platform/token",
|
||||
"documentation/platform/identities/universal-auth",
|
||||
"documentation/platform/identities/gcp-auth",
|
||||
"documentation/platform/identities/aws-auth",
|
||||
"documentation/platform/mfa",
|
||||
{
|
||||
|
@ -120,7 +120,7 @@ export default function NavHeader({
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{
|
||||
pathname: "/project/[id]/secrets/v2/[env]",
|
||||
pathname: "/project/[id]/secrets/[env]",
|
||||
query: { id: router.query.id, env: router.query.env }
|
||||
}}
|
||||
>
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { forwardRef, HTMLAttributes } from "react";
|
||||
|
||||
type Props = {
|
||||
symbolName: string;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const FontAwesomeSymbol = forwardRef<HTMLDivElement, Props>(
|
||||
({ symbolName, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...props}>
|
||||
<svg className="w-inherit h-inherit">
|
||||
<use href={`#${symbolName}`} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FontAwesomeSymbol.displayName = "FontAwesomeSymbol";
|
1
frontend/src/components/v2/FontAwesomeSymbol/index.tsx
Normal file
1
frontend/src/components/v2/FontAwesomeSymbol/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { FontAwesomeSymbol } from "./FontAwesomeSymbol";
|
@ -1,17 +1,42 @@
|
||||
import { TextareaHTMLAttributes, useEffect, useRef, useState } from "react";
|
||||
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useGetFoldersByEnv, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
|
||||
import { useDebounce, useToggle } from "@app/hooks";
|
||||
import { useGetProjectFolders, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
|
||||
|
||||
import { SecretInput } from "../SecretInput";
|
||||
|
||||
const REGEX_UNCLOSED_SECRET_REFERENCE = /\${(?![^{}]*\})/g;
|
||||
const REGEX_OPEN_SECRET_REFERENCE = /\${/g;
|
||||
const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => {
|
||||
// take substring up to pos in order to consider edits for closed references
|
||||
for (let i = pos; i >= 1; i -= 1) {
|
||||
if (value[i] === "}") return -1;
|
||||
if (value[i - 1] === "$" && value[i] === "{") {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
const getIndexOfUnclosedRefToTheRight = (value: string, pos: number) => {
|
||||
// use it with above to identify an open ${
|
||||
for (let i = pos; i < value.length; i += 1) {
|
||||
if (value[i] === "}") return i - 1;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
const getClosingSymbol = (isSelectedSecret: boolean, isClosed: boolean) => {
|
||||
if (!isClosed) {
|
||||
return isSelectedSecret ? "}" : ".";
|
||||
}
|
||||
if (!isSelectedSecret) return ".";
|
||||
return "";
|
||||
};
|
||||
|
||||
const mod = (n: number, m: number) => ((n % m) + m) % m;
|
||||
|
||||
export enum ReferenceType {
|
||||
ENVIRONMENT = "environment",
|
||||
@ -19,8 +44,9 @@ export enum ReferenceType {
|
||||
SECRET = "secret"
|
||||
}
|
||||
|
||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
value?: string | null;
|
||||
type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "value"> & {
|
||||
value?: string;
|
||||
onChange: (val: string) => void;
|
||||
isImport?: boolean;
|
||||
isVisible?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
@ -31,339 +57,298 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
};
|
||||
|
||||
type ReferenceItem = {
|
||||
name: string;
|
||||
label: string;
|
||||
type: ReferenceType;
|
||||
slug?: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export const InfisicalSecretInput = ({
|
||||
value: propValue,
|
||||
containerClassName,
|
||||
secretPath: propSecretPath,
|
||||
environment: propEnvironment,
|
||||
onChange,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [inputValue, setInputValue] = useState(propValue ?? "");
|
||||
const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false);
|
||||
const [currentCursorPosition, setCurrentCursorPosition] = useState(0);
|
||||
const [currentReference, setCurrentReference] = useState<string>("");
|
||||
const [secretPath, setSecretPath] = useState<string>(propSecretPath || "/");
|
||||
const [environment, setEnvironment] = useState<string | undefined>(propEnvironment);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||
const { data: secrets } = useGetProjectSecrets({
|
||||
decryptFileKey: decryptFileKey!,
|
||||
environment: environment || currentWorkspace?.environments?.[0].slug!,
|
||||
secretPath,
|
||||
workspaceId
|
||||
});
|
||||
const { folderNames: folders } = useGetFoldersByEnv({
|
||||
path: secretPath,
|
||||
environments: [environment || currentWorkspace?.environments?.[0].slug!],
|
||||
projectId: workspaceId
|
||||
});
|
||||
export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
(
|
||||
{
|
||||
value = "",
|
||||
onChange,
|
||||
containerClassName,
|
||||
secretPath: propSecretPath,
|
||||
environment: propEnvironment,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
const debouncedCurrentReference = useDebounce(currentReference, 100);
|
||||
const debouncedValue = useDebounce(value, 500);
|
||||
|
||||
const [listReference, setListReference] = useState<ReferenceItem[]>([]);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const isPopupOpen = isSuggestionsOpen && listReference.length > 0 && currentReference.length > 0;
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(propValue ?? "");
|
||||
}, [propValue]);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const popoverContentRef = useRef<HTMLDivElement>(null);
|
||||
const [isFocused, setIsFocused] = useToggle(false);
|
||||
const currentCursorPosition = inputRef.current?.selectionStart || 0;
|
||||
|
||||
useEffect(() => {
|
||||
let currentEnvironment = propEnvironment;
|
||||
let currentSecretPath = propSecretPath || "/";
|
||||
const suggestionSource = useMemo(() => {
|
||||
const left = getIndexOfUnclosedRefToTheLeft(debouncedValue, currentCursorPosition - 1);
|
||||
if (left === -1) return { left, value: "", predicate: "", isDeep: false };
|
||||
|
||||
if (!currentReference) {
|
||||
setSecretPath(currentSecretPath);
|
||||
setEnvironment(currentEnvironment);
|
||||
return;
|
||||
}
|
||||
const suggestionSourceValue = debouncedValue.slice(left + 1, currentCursorPosition);
|
||||
let suggestionSourceEnv: string | undefined = propEnvironment;
|
||||
let suggestionSourceSecretPath: string | undefined = propSecretPath || "/";
|
||||
|
||||
const isNested = currentReference.includes(".");
|
||||
// means its like <environment>.<folder1>.<...more folder>.secret
|
||||
const isDeep = suggestionSourceValue.includes(".");
|
||||
let predicate = suggestionSourceValue;
|
||||
if (isDeep) {
|
||||
const [envSlug, ...folderPaths] = suggestionSourceValue.split(".");
|
||||
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
|
||||
suggestionSourceEnv = isValidEnvSlug ? envSlug : undefined;
|
||||
suggestionSourceSecretPath = `/${folderPaths.slice(0, -1)?.join("/")}`;
|
||||
predicate = folderPaths[folderPaths.length - 1];
|
||||
}
|
||||
|
||||
if (isNested) {
|
||||
const [envSlug, ...folderPaths] = currentReference.split(".");
|
||||
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
|
||||
currentEnvironment = isValidEnvSlug ? envSlug : undefined;
|
||||
return {
|
||||
left: left + 1,
|
||||
// the full value inside a ${<value>}
|
||||
value: suggestionSourceValue,
|
||||
// the final part after staging.dev.<folder1>.<predicate>
|
||||
predicate,
|
||||
isOpen: left !== -1,
|
||||
isDeep,
|
||||
environment: suggestionSourceEnv,
|
||||
secretPath: suggestionSourceSecretPath
|
||||
};
|
||||
}, [debouncedValue]);
|
||||
|
||||
// should be based on the last valid section (with .)
|
||||
folderPaths.pop();
|
||||
currentSecretPath = `/${folderPaths?.join("/")}`;
|
||||
}
|
||||
|
||||
setSecretPath(currentSecretPath);
|
||||
setEnvironment(currentEnvironment);
|
||||
}, [debouncedCurrentReference]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentListReference: ReferenceItem[] = [];
|
||||
const isNested = currentReference?.includes(".");
|
||||
|
||||
if (!currentReference) {
|
||||
setListReference(currentListReference);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
currentWorkspace?.environments.forEach((env) => {
|
||||
currentListReference.unshift({
|
||||
name: env.slug,
|
||||
type: ReferenceType.ENVIRONMENT
|
||||
});
|
||||
});
|
||||
} else if (isNested) {
|
||||
folders?.forEach((folder) => {
|
||||
currentListReference.unshift({ name: folder, type: ReferenceType.FOLDER });
|
||||
});
|
||||
} else if (environment) {
|
||||
currentWorkspace?.environments.forEach((env) => {
|
||||
currentListReference.unshift({
|
||||
name: env.slug,
|
||||
type: ReferenceType.ENVIRONMENT
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
secrets?.forEach((secret) => {
|
||||
currentListReference.unshift({ name: secret.key, type: ReferenceType.SECRET });
|
||||
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
|
||||
const { data: secrets } = useGetProjectSecrets({
|
||||
decryptFileKey: decryptFileKey!,
|
||||
environment: suggestionSource.environment || "",
|
||||
secretPath: suggestionSource.secretPath || "",
|
||||
workspaceId,
|
||||
options: {
|
||||
enabled: isPopupOpen
|
||||
}
|
||||
});
|
||||
|
||||
// Get fragment inside currentReference
|
||||
const searchFragment = isNested ? currentReference.split(".").pop() || "" : currentReference;
|
||||
const filteredListRef = currentListReference
|
||||
.filter((suggestionEntry) =>
|
||||
suggestionEntry.name.toUpperCase().startsWith(searchFragment.toUpperCase())
|
||||
)
|
||||
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
||||
|
||||
setListReference(filteredListRef);
|
||||
}, [secrets, environment, debouncedCurrentReference]);
|
||||
|
||||
const getIndexOfUnclosedRefToTheLeft = (pos: number) => {
|
||||
// take substring up to pos in order to consider edits for closed references
|
||||
const unclosedReferenceIndexMatches = [
|
||||
...inputValue.substring(0, pos).matchAll(REGEX_UNCLOSED_SECRET_REFERENCE)
|
||||
].map((match) => match.index);
|
||||
|
||||
// find unclosed reference index less than the current cursor position
|
||||
let indexIter = -1;
|
||||
unclosedReferenceIndexMatches.forEach((index) => {
|
||||
if (index !== undefined && index > indexIter && index < pos) {
|
||||
indexIter = index;
|
||||
const { data: folders } = useGetProjectFolders({
|
||||
environment: suggestionSource.environment || "",
|
||||
path: suggestionSource.secretPath || "",
|
||||
projectId: workspaceId,
|
||||
options: {
|
||||
enabled: isPopupOpen
|
||||
}
|
||||
});
|
||||
|
||||
return indexIter;
|
||||
};
|
||||
const suggestions = useMemo(() => {
|
||||
if (!isPopupOpen) return [];
|
||||
// reset highlight whenever recomputation happens
|
||||
setHighlightedIndex(-1);
|
||||
const suggestionsArr: ReferenceItem[] = [];
|
||||
const predicate = suggestionSource.predicate.toLowerCase();
|
||||
|
||||
const getIndexOfUnclosedRefToTheRight = (pos: number) => {
|
||||
const unclosedReferenceIndexMatches = [...inputValue.matchAll(REGEX_OPEN_SECRET_REFERENCE)].map(
|
||||
(match) => match.index
|
||||
);
|
||||
|
||||
// find the next unclosed reference index to the right of the current cursor position
|
||||
// this is so that we know the limitation for slicing references
|
||||
let indexIter = Infinity;
|
||||
unclosedReferenceIndexMatches.forEach((index) => {
|
||||
if (index !== undefined && index > pos && index < indexIter) {
|
||||
indexIter = index;
|
||||
if (!suggestionSource.isDeep) {
|
||||
// At first level only environments and secrets
|
||||
(currentWorkspace?.environments || []).forEach(({ name, slug }) => {
|
||||
if (name.toLowerCase().startsWith(predicate))
|
||||
suggestionsArr.push({
|
||||
label: name,
|
||||
slug,
|
||||
type: ReferenceType.ENVIRONMENT
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// one deeper levels its based on an environment folders and secrets
|
||||
(folders || []).forEach(({ name }) => {
|
||||
if (name.toLowerCase().startsWith(predicate))
|
||||
suggestionsArr.push({
|
||||
label: name,
|
||||
slug: name,
|
||||
type: ReferenceType.FOLDER
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
(secrets || []).forEach(({ key }) => {
|
||||
if (key.toLowerCase().startsWith(predicate))
|
||||
suggestionsArr.push({
|
||||
label: key,
|
||||
slug: key,
|
||||
type: ReferenceType.SECRET
|
||||
});
|
||||
});
|
||||
return suggestionsArr;
|
||||
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
|
||||
|
||||
return indexIter;
|
||||
};
|
||||
const handleSuggestionSelect = (selectIndex?: number) => {
|
||||
const selectedSuggestion =
|
||||
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
|
||||
if (!selectedSuggestion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// open suggestions if current position is to the right of an unclosed secret reference
|
||||
const indexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
|
||||
if (indexIter === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSuggestionsOpen(true);
|
||||
|
||||
if (e.key !== "Enter") {
|
||||
// current reference is then going to be based on the text from the closest ${ to the right
|
||||
// until the current cursor position
|
||||
const openReferenceValue = inputValue.slice(indexIter + 2, currentCursorPosition);
|
||||
setCurrentReference(openReferenceValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestionSelect = (selectedIndex?: number) => {
|
||||
const selectedSuggestion = listReference[selectedIndex ?? highlightedIndex];
|
||||
|
||||
if (!selectedSuggestion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftIndexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
|
||||
const rightIndexLimit = getIndexOfUnclosedRefToTheRight(currentCursorPosition);
|
||||
|
||||
if (leftIndexIter === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newValue = "";
|
||||
const currentOpenRef = inputValue.slice(leftIndexIter + 2, currentCursorPosition);
|
||||
if (currentOpenRef.includes(".")) {
|
||||
// append suggestion after last DOT (.)
|
||||
const lastDotIndex = currentReference.lastIndexOf(".");
|
||||
const existingPath = currentReference.slice(0, lastDotIndex);
|
||||
const refEndAfterAppending = Math.min(
|
||||
leftIndexIter +
|
||||
3 +
|
||||
existingPath.length +
|
||||
selectedSuggestion.name.length +
|
||||
Number(selectedSuggestion.type !== ReferenceType.SECRET),
|
||||
rightIndexLimit - 1
|
||||
const rightBracketIndex = getIndexOfUnclosedRefToTheRight(value, suggestionSource.left);
|
||||
const isEnclosed = rightBracketIndex !== -1;
|
||||
// <lhsValue>${}<rhsvalue>
|
||||
const lhsValue = value.slice(0, suggestionSource.left);
|
||||
const rhsValue = value.slice(
|
||||
rightBracketIndex !== -1 ? rightBracketIndex + 1 : currentCursorPosition
|
||||
);
|
||||
// mid will be computed value inside the interpolation
|
||||
const mid = suggestionSource.isDeep
|
||||
? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${selectedSuggestion.slug
|
||||
}`
|
||||
: selectedSuggestion.slug;
|
||||
// whether we should append . or closing bracket on selecting suggestion
|
||||
const closingSymbol = getClosingSymbol(
|
||||
selectedSuggestion.type === ReferenceType.SECRET,
|
||||
isEnclosed
|
||||
);
|
||||
|
||||
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${existingPath}.${
|
||||
selectedSuggestion.name
|
||||
}${selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"}${inputValue.slice(
|
||||
refEndAfterAppending
|
||||
)}`;
|
||||
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
|
||||
setCurrentReference(openReferenceValue);
|
||||
const newValue = `${lhsValue}${mid}${closingSymbol}${rhsValue}`;
|
||||
onChange?.(newValue);
|
||||
// this delay is for cursor adjustment
|
||||
// cannot do this without a delay because what happens in onChange gets propogated after the cursor change
|
||||
// Thus the cursor goes last to avoid that we put a slight delay on cursor change to make it happen later
|
||||
const delay = setTimeout(() => {
|
||||
clearTimeout(delay);
|
||||
if (inputRef.current)
|
||||
inputRef.current.selectionEnd =
|
||||
lhsValue.length +
|
||||
mid.length +
|
||||
closingSymbol.length +
|
||||
(isEnclosed && selectedSuggestion.type === ReferenceType.SECRET ? 1 : 0); // if secret is selected the cursor should move after the closing bracket -> }
|
||||
}, 10);
|
||||
setHighlightedIndex(-1); // reset highlight
|
||||
};
|
||||
|
||||
// add 1 in order to prevent referenceOpen from being triggered by handleKeyUp
|
||||
setCurrentCursorPosition(refEndAfterAppending + 1);
|
||||
} else {
|
||||
// append selectedSuggestion at position after unclosed ${
|
||||
const refEndAfterAppending = Math.min(
|
||||
selectedSuggestion.name.length +
|
||||
leftIndexIter +
|
||||
2 +
|
||||
Number(selectedSuggestion.type !== ReferenceType.SECRET),
|
||||
rightIndexLimit - 1
|
||||
);
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// key operation should trigger only when popup is open
|
||||
if (isPopupOpen) {
|
||||
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
|
||||
setHighlightedIndex((prevIndex) => {
|
||||
const pos = mod(prevIndex + 1, suggestions.length);
|
||||
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth"
|
||||
});
|
||||
return pos;
|
||||
});
|
||||
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
|
||||
setHighlightedIndex((prevIndex) => {
|
||||
const pos = mod(prevIndex - 1, suggestions.length);
|
||||
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth"
|
||||
});
|
||||
return pos;
|
||||
});
|
||||
} else if (e.key === "Enter" && highlightedIndex >= 0) {
|
||||
e.preventDefault();
|
||||
handleSuggestionSelect();
|
||||
}
|
||||
if (["ArrowDown", "ArrowUp", "Tab"].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${selectedSuggestion.name}${
|
||||
selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"
|
||||
}${inputValue.slice(refEndAfterAppending)}`;
|
||||
const handlePopUpOpen = () => {
|
||||
setHighlightedIndex(-1);
|
||||
};
|
||||
|
||||
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
|
||||
setCurrentReference(openReferenceValue);
|
||||
setCurrentCursorPosition(refEndAfterAppending);
|
||||
}
|
||||
// to handle multiple ref for single component
|
||||
const handleRef = useCallback((el: HTMLTextAreaElement) => {
|
||||
// @ts-expect-error this is for multiple ref single component
|
||||
inputRef.current = el;
|
||||
if (ref) {
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
ref.current = el;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
onChange?.({ target: { value: newValue } } as any);
|
||||
setInputValue(newValue);
|
||||
setHighlightedIndex(-1);
|
||||
setIsSuggestionsOpen(false);
|
||||
};
|
||||
return (
|
||||
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<SecretInput
|
||||
{...props}
|
||||
ref={handleRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onBlur={(evt) => {
|
||||
// should not on blur when its mouse down selecting a item from suggestion
|
||||
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
|
||||
setIsFocused.off();
|
||||
}}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
align="start"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
className="relative top-2 z-[100] max-h-64 overflow-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white"
|
||||
ref={popoverContentRef}
|
||||
>
|
||||
{suggestions.map((item, i) => {
|
||||
let entryIcon;
|
||||
if (item.type === ReferenceType.SECRET) {
|
||||
entryIcon = faKey;
|
||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||
entryIcon = faCircle;
|
||||
} else {
|
||||
entryIcon = faFolder;
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const mod = (n: number, m: number) => ((n % m) + m) % m;
|
||||
if (e.key === "ArrowDown") {
|
||||
setHighlightedIndex((prevIndex) => mod(prevIndex + 1, listReference.length));
|
||||
} else if (e.key === "ArrowUp") {
|
||||
setHighlightedIndex((prevIndex) => mod(prevIndex - 1, listReference.length));
|
||||
} else if (e.key === "Enter" && highlightedIndex >= 0) {
|
||||
handleSuggestionSelect();
|
||||
}
|
||||
|
||||
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key) && isPopupOpen) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const setIsOpen = (isOpen: boolean) => {
|
||||
setHighlightedIndex(-1);
|
||||
|
||||
if (isSuggestionsOpen) {
|
||||
setIsSuggestionsOpen(isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretChange = (e: any) => {
|
||||
// propagate event to react-hook-form onChange
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
|
||||
setCurrentCursorPosition(inputRef.current?.selectionStart || 0);
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root open={isPopupOpen} onOpenChange={setIsOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<SecretInput
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
value={inputValue}
|
||||
onChange={handleSecretChange}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
align="start"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
className={twMerge(
|
||||
"relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
|
||||
)}
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
maxHeight: "var(--radix-select-content-available-height)"
|
||||
}}
|
||||
>
|
||||
<div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white">
|
||||
{listReference.map((item, i) => {
|
||||
let entryIcon;
|
||||
if (item.type === ReferenceType.SECRET) {
|
||||
entryIcon = faKey;
|
||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||
entryIcon = faCircle;
|
||||
} else {
|
||||
entryIcon = faFolder;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
setHighlightedIndex(i);
|
||||
handleSuggestionSelect(i);
|
||||
}}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
className="flex items-center justify-between border-mineshaft-600 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
highlightedIndex === i ? "bg-gray-600" : ""
|
||||
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSuggestionSelect(i);
|
||||
}}
|
||||
aria-label="suggestion-item"
|
||||
onClick={(e) => {
|
||||
inputRef.current?.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSuggestionSelect(i);
|
||||
}}
|
||||
onMouseEnter={() => setHighlightedIndex(i)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
className="flex items-center justify-between border-mineshaft-600 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="flex items-center text-yellow-700">
|
||||
<FontAwesomeIcon
|
||||
icon={entryIcon}
|
||||
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
|
||||
/>
|
||||
<div
|
||||
className={`${highlightedIndex === i ? "bg-gray-600" : ""
|
||||
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
|
||||
>
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="flex items-center text-yellow-700">
|
||||
<FontAwesomeIcon
|
||||
icon={entryIcon}
|
||||
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-md w-10/12 truncate text-left">{item.label}</div>
|
||||
</div>
|
||||
<div className="text-md w-10/12 truncate text-left">{item.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
);
|
||||
};
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InfisicalSecretInput.displayName = "InfisicalSecretInput";
|
||||
|
26
frontend/src/components/v2/NoticeBanner/NoticeBanner.tsx
Normal file
26
frontend/src/components/v2/NoticeBanner/NoticeBanner.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faWarning, IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
icon?: IconDefinition;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const NoticeBanner = ({ icon = faWarning, title, children, className }: Props) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} className="pr-6 text-4xl text-white/80" />
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<div className="mb-2 text-lg font-semibold">{title}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
1
frontend/src/components/v2/NoticeBanner/index.tsx
Normal file
1
frontend/src/components/v2/NoticeBanner/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NoticeBanner } from "./NoticeBanner";
|
@ -41,7 +41,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
||||
|
||||
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
|
||||
// when break is added a line break works properly
|
||||
return formattedContent.concat(<br />);
|
||||
return formattedContent.concat(<br key={`secret-value-${formattedContent.length + 1}`} />);
|
||||
};
|
||||
|
||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
@ -90,7 +90,10 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
aria-label="secret value"
|
||||
ref={ref}
|
||||
className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`}
|
||||
onFocus={() => setIsSecretFocused.on()}
|
||||
onFocus={(evt) => {
|
||||
onFocus?.(evt);
|
||||
setIsSecretFocused.on();
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
spellCheck={false}
|
||||
onBlur={(evt) => {
|
||||
|
@ -10,12 +10,14 @@ export * from "./Drawer";
|
||||
export * from "./Dropdown";
|
||||
export * from "./EmailServiceSetupModal";
|
||||
export * from "./EmptyState";
|
||||
export * from "./FontAwesomeSymbol";
|
||||
export * from "./FormControl";
|
||||
export * from "./HoverCardv2";
|
||||
export * from "./IconButton";
|
||||
export * from "./Input";
|
||||
export * from "./Menu";
|
||||
export * from "./Modal";
|
||||
export * from "./NoticeBanner";
|
||||
export * from "./Pagination";
|
||||
export * from "./Popoverv2";
|
||||
export * from "./SecretInput";
|
||||
|
@ -5,6 +5,7 @@ export type TServerConfig = {
|
||||
isMigrationModeOn?: boolean;
|
||||
trustSamlEmails: boolean;
|
||||
trustLdapEmails: boolean;
|
||||
isSecretScanningDisabled: boolean;
|
||||
};
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
|
@ -2,6 +2,5 @@ import { IdentityAuthMethod } from "./enums";
|
||||
|
||||
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
|
||||
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
|
||||
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth"
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
export enum IdentityAuthMethod {
|
||||
UNIVERSAL_AUTH = "universal-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth"
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ export { identityAuthToNameMap } from "./constants";
|
||||
export { IdentityAuthMethod } from "./enums";
|
||||
export {
|
||||
useAddIdentityAwsAuth,
|
||||
useAddIdentityGcpAuth,
|
||||
useAddIdentityUniversalAuth,
|
||||
useCreateIdentity,
|
||||
useCreateIdentityUniversalAuthClientSecret,
|
||||
@ -10,11 +9,10 @@ export {
|
||||
useRevokeIdentityUniversalAuthClientSecret,
|
||||
useUpdateIdentity,
|
||||
useUpdateIdentityAwsAuth,
|
||||
useUpdateIdentityGcpAuth,
|
||||
useUpdateIdentityUniversalAuth} from "./mutations";
|
||||
useUpdateIdentityUniversalAuth
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetIdentityAwsAuth,
|
||||
useGetIdentityGcpAuth,
|
||||
useGetIdentityUniversalAuth,
|
||||
useGetIdentityUniversalAuthClientSecrets
|
||||
} from "./queries";
|
||||
|
@ -6,7 +6,6 @@ import { organizationKeys } from "../organization/queries";
|
||||
import { identitiesKeys } from "./queries";
|
||||
import {
|
||||
AddIdentityAwsAuthDTO,
|
||||
AddIdentityGcpAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
ClientSecretData,
|
||||
CreateIdentityDTO,
|
||||
@ -16,11 +15,9 @@ import {
|
||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||
Identity,
|
||||
IdentityAwsAuth,
|
||||
IdentityGcpAuth,
|
||||
IdentityUniversalAuth,
|
||||
UpdateIdentityAwsAuthDTO,
|
||||
UpdateIdentityDTO,
|
||||
UpdateIdentityGcpAuthDTO,
|
||||
UpdateIdentityUniversalAuthDTO
|
||||
} from "./types";
|
||||
|
||||
@ -176,82 +173,6 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityGcpAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityGcpAuth, {}, AddIdentityGcpAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityGcpAuth }
|
||||
} = await apiRequest.post<{ identityGcpAuth: IdentityGcpAuth }>(
|
||||
`/api/v1/auth/gcp-auth/identities/${identityId}`,
|
||||
{
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityGcpAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityGcpAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityGcpAuth, {}, UpdateIdentityGcpAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityGcpAuth }
|
||||
} = await apiRequest.patch<{ identityGcpAuth: IdentityGcpAuth }>(
|
||||
`/api/v1/auth/gcp-auth/identities/${identityId}`,
|
||||
{
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityGcpAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityAwsAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityAwsAuth, {}, AddIdentityAwsAuthDTO>({
|
||||
@ -315,7 +236,6 @@ export const useUpdateIdentityAwsAuth = () => {
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityAwsAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
|
@ -2,14 +2,13 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { ClientSecretData, IdentityAwsAuth, IdentityGcpAuth, IdentityUniversalAuth } from "./types";
|
||||
import { ClientSecretData, IdentityAwsAuth, IdentityUniversalAuth } from "./types";
|
||||
|
||||
export const identitiesKeys = {
|
||||
getIdentityUniversalAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-universal-auth"] as const,
|
||||
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
|
||||
[{ identityId }, "identity-universal-auth-client-secrets"] as const,
|
||||
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
|
||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const
|
||||
};
|
||||
|
||||
@ -43,21 +42,6 @@ export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) =>
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityGcpAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
queryKey: identitiesKeys.getIdentityGcpAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityGcpAuth }
|
||||
} = await apiRequest.get<{ identityGcpAuth: IdentityGcpAuth }>(
|
||||
`/api/v1/auth/gcp-auth/identities/${identityId}`
|
||||
);
|
||||
return identityGcpAuth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityAwsAuth = (identityId: string) => {
|
||||
return useQuery({
|
||||
enabled: Boolean(identityId),
|
||||
|
@ -113,48 +113,6 @@ export type UpdateIdentityUniversalAuthDTO = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityGcpAuth = {
|
||||
identityId: string;
|
||||
type: "iam" | "gce";
|
||||
allowedServiceAccounts: string;
|
||||
allowedProjects: string;
|
||||
allowedZones: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityGcpAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
type: "iam" | "gce";
|
||||
allowedServiceAccounts: string;
|
||||
allowedProjects: string;
|
||||
allowedZones: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityGcpAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
type?: "iam" | "gce";
|
||||
allowedServiceAccounts?: string;
|
||||
allowedProjects?: string;
|
||||
allowedZones?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IdentityAwsAuth = {
|
||||
identityId: string;
|
||||
type: "iam";
|
||||
|
@ -21,9 +21,7 @@ import {
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faUserPlus,
|
||||
faWarning,
|
||||
faXmark
|
||||
faUserPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
@ -56,7 +54,6 @@ import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetUserAction,
|
||||
useRegisterUserAction
|
||||
} from "@app/hooks/api";
|
||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
@ -312,9 +309,8 @@ const LearningItem = ({
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} mb-3 rounded-md`}
|
||||
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} mb-3 rounded-md`}
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@ -325,11 +321,10 @@ const LearningItem = ({
|
||||
await registerUserAction.mutateAsync(userAction);
|
||||
}
|
||||
}}
|
||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||
complete
|
||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete
|
||||
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||
} text-mineshaft-100 duration-200`}
|
||||
} text-mineshaft-100 duration-200`}
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
@ -407,9 +402,8 @@ const LearningItemSquare = ({
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} w-full rounded-md`}
|
||||
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||
} w-full rounded-md`}
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@ -420,11 +414,10 @@ const LearningItemSquare = ({
|
||||
await registerUserAction.mutateAsync(userAction);
|
||||
}
|
||||
}}
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
|
||||
complete
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete
|
||||
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
||||
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
|
||||
} text-mineshaft-100 duration-200`}
|
||||
} text-mineshaft-100 duration-200`}
|
||||
>
|
||||
<div className="flex w-full flex-col items-center px-6 py-4">
|
||||
<div className="flex w-full flex-row items-start justify-between">
|
||||
@ -438,9 +431,8 @@ const LearningItemSquare = ({
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`text-right text-sm font-normal text-mineshaft-300 ${
|
||||
complete ? "font-semibold text-primary" : ""
|
||||
}`}
|
||||
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "font-semibold text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
@ -480,14 +472,8 @@ const OrganizationPage = withPermission(
|
||||
const { currentOrg } = useOrganization();
|
||||
const routerOrgId = String(router.query.id);
|
||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
||||
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
|
||||
const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed");
|
||||
const registerUserAction = useRegisterUserAction();
|
||||
const closeUpdate = async () => {
|
||||
await registerUserAction.mutateAsync("april_13_2024_db_update_closed");
|
||||
};
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
@ -594,31 +580,6 @@ const OrganizationPage = withPermission(
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||
{(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080")) && (
|
||||
<div
|
||||
className={`${
|
||||
!updateClosed ? "block" : "hidden"
|
||||
} mb-4 flex w-full flex-row items-center rounded-md border border-primary-600 bg-primary/10 p-2 text-base text-white`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} className="p-6 text-4xl text-primary" />
|
||||
<div className="text-sm">
|
||||
<span className="text-lg font-semibold">Scheduled maintenance on May 11th 2024 </span>{" "}
|
||||
<br />
|
||||
Infisical will undergo scheduled maintenance for approximately 2 hour on Saturday, May 11th, 11am EST. During these hours, read
|
||||
operations to Infisical will continue to function normally but no resources will be editable.
|
||||
No action is required on your end — your applications will continue to fetch secrets.
|
||||
<br />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => closeUpdate()}
|
||||
aria-label="close"
|
||||
className="flex h-full items-start text-mineshaft-100 duration-200 hover:text-red-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
</div>)}
|
||||
|
||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||
<div className="mt-6 flex w-full flex-row">
|
||||
<Input
|
||||
@ -748,95 +709,94 @@ const OrganizationPage = withPermission(
|
||||
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
||||
30 * 24 * 60 * 60 * 1000
|
||||
) && (
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<LearningItemSquare
|
||||
text="Watch Infisical demo"
|
||||
subText="Set up Infisical in 3 min."
|
||||
complete={hasUserClickedIntro}
|
||||
icon={faHandPeace}
|
||||
time="3 min"
|
||||
userAction="intro_cta_clicked"
|
||||
link="https://www.youtube.com/watch?v=PK23097-25I"
|
||||
/>
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<>
|
||||
<LearningItemSquare
|
||||
text="Add your secrets"
|
||||
subText="Drop a .env file or type your secrets."
|
||||
complete={hasUserPushedSecrets}
|
||||
icon={faPlus}
|
||||
time="1 min"
|
||||
userAction="first_time_secrets_pushed"
|
||||
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
|
||||
/>
|
||||
<LearningItemSquare
|
||||
text="Invite your teammates"
|
||||
subText="Infisical is better used as a team."
|
||||
complete={usersInOrg}
|
||||
icon={faUserPlus}
|
||||
time="2 min"
|
||||
link={`/org/${router.query.id}/members?action=invite`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="block xl:hidden 2xl:block">
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<LearningItemSquare
|
||||
text="Join Infisical Slack"
|
||||
subText="Have any questions? Ask us!"
|
||||
complete={hasUserClickedSlack}
|
||||
icon={faSlack}
|
||||
time="1 min"
|
||||
userAction="slack_cta_clicked"
|
||||
link="https://infisical.com/slack"
|
||||
text="Watch Infisical demo"
|
||||
subText="Set up Infisical in 3 min."
|
||||
complete={hasUserClickedIntro}
|
||||
icon={faHandPeace}
|
||||
time="3 min"
|
||||
userAction="intro_cta_clicked"
|
||||
link="https://www.youtube.com/watch?v=PK23097-25I"
|
||||
/>
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<>
|
||||
<LearningItemSquare
|
||||
text="Add your secrets"
|
||||
subText="Drop a .env file or type your secrets."
|
||||
complete={hasUserPushedSecrets}
|
||||
icon={faPlus}
|
||||
time="1 min"
|
||||
userAction="first_time_secrets_pushed"
|
||||
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
|
||||
/>
|
||||
<LearningItemSquare
|
||||
text="Invite your teammates"
|
||||
subText="Infisical is better used as a team."
|
||||
complete={usersInOrg}
|
||||
icon={faUserPlus}
|
||||
time="2 min"
|
||||
link={`/org/${router.query.id}/members?action=invite`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="block xl:hidden 2xl:block">
|
||||
<LearningItemSquare
|
||||
text="Join Infisical Slack"
|
||||
subText="Have any questions? Ask us!"
|
||||
complete={hasUserClickedSlack}
|
||||
icon={faSlack}
|
||||
time="1 min"
|
||||
userAction="slack_cta_clicked"
|
||||
link="https://infisical.com/slack"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="h-5 w-5 text-4xl text-green"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="h-5 w-5 text-4xl text-green"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
|
||||
}`}
|
||||
>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-28 pr-4 text-right text-sm font-semibold ${
|
||||
false && "text-green"
|
||||
}`}
|
||||
>
|
||||
About 2 min
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>
|
||||
)}
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<LearningItem
|
||||
text="Integrate Infisical with your infrastructure"
|
||||
subText="Connect Infisical to various 3rd party services and platforms."
|
||||
complete={false}
|
||||
icon={faPlug}
|
||||
time="15 min"
|
||||
link="https://infisical.com/docs/integrations/overview"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{orgWorkspaces.length !== 0 && (
|
||||
<LearningItem
|
||||
text="Integrate Infisical with your infrastructure"
|
||||
subText="Connect Infisical to various 3rd party services and platforms."
|
||||
complete={false}
|
||||
icon={faPlug}
|
||||
time="15 min"
|
||||
link="https://infisical.com/docs/integrations/overview"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isModalOpen) => {
|
||||
|
@ -3,8 +3,8 @@ import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { Button, NoticeBanner } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useServerConfig } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||
|
||||
@ -17,6 +17,7 @@ const SecretScanning = withPermission(
|
||||
const router = useRouter();
|
||||
const queryParams = router.query;
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false);
|
||||
const { config } = useServerConfig();
|
||||
|
||||
useEffect(() => {
|
||||
const linkInstallation = async () => {
|
||||
@ -69,6 +70,11 @@ const SecretScanning = withPermission(
|
||||
<div className="mb-6 text-lg text-mineshaft-300">
|
||||
Automatically monitor your GitHub activity and prevent secret leaks
|
||||
</div>
|
||||
{config.isSecretScanningDisabled && (
|
||||
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
|
||||
We are working on improving the performance of secret scanning due to increased usage.
|
||||
</NoticeBanner>
|
||||
)}
|
||||
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mb-1 flex flex-row">
|
||||
@ -110,7 +116,7 @@ const SecretScanning = withPermission(
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="h-min py-2"
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || config.isSecretScanningDisabled}
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
|
@ -45,6 +45,14 @@ html {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.w-inherit {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.h-inherit {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
@ -15,7 +15,6 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
|
||||
type Props = {
|
||||
@ -29,7 +28,6 @@ type Props = {
|
||||
|
||||
const identityAuthMethods = [
|
||||
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
|
||||
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
|
||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }
|
||||
];
|
||||
|
||||
@ -77,15 +75,6 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.GCP_AUTH: {
|
||||
return (
|
||||
<IdentityGcpAuthForm
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
identityAuthMethodData={identityAuthMethodData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||
return (
|
||||
<IdentityUniversalAuthForm
|
||||
|
@ -1,384 +0,0 @@
|
||||
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, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityGcpAuth,
|
||||
useGetIdentityGcpAuth,
|
||||
useUpdateIdentityGcpAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
type: z.enum(["iam", "gce"]),
|
||||
allowedServiceAccounts: z.string(),
|
||||
allowedProjects: z.string(),
|
||||
allowedZones: z.string(),
|
||||
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"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
identityAuthMethodData: {
|
||||
identityId: string;
|
||||
name: string;
|
||||
authMethod?: IdentityAuthMethod;
|
||||
};
|
||||
};
|
||||
|
||||
export const IdentityGcpAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityAuthMethodData
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
||||
|
||||
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "");
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: "gce",
|
||||
allowedServiceAccounts: "",
|
||||
allowedProjects: "",
|
||||
allowedZones: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
}
|
||||
});
|
||||
|
||||
const watchedType = watch("type");
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
remove: removeAccessTokenTrustedIp
|
||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
type: data.type,
|
||||
allowedServiceAccounts: data.allowedServiceAccounts,
|
||||
allowedProjects: data.allowedProjects,
|
||||
allowedZones: data.allowedZones,
|
||||
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({
|
||||
type: "iam",
|
||||
allowedServiceAccounts: "",
|
||||
allowedProjects: "",
|
||||
allowedZones: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityAuthMethodData) return;
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId,
|
||||
type,
|
||||
allowedServiceAccounts,
|
||||
allowedProjects,
|
||||
allowedZones,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
identityId: identityAuthMethodData.identityId,
|
||||
organizationId: orgId,
|
||||
type,
|
||||
allowedServiceAccounts: allowedServiceAccounts || "",
|
||||
allowedProjects: allowedProjects || "",
|
||||
allowedZones: allowedZones || "",
|
||||
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="type"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="gce" key="gcp-type-gce">
|
||||
GCP ID Token Auth (Recommended)
|
||||
</SelectItem>
|
||||
<SelectItem value="iam" key="gcpiam">
|
||||
GCP IAM Auth
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="allowedServiceAccounts"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Service Account Emails"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="test@project.iam.gserviceaccount.com, 12345-compute@developer.gserviceaccount.com"
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedProjects"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Projects"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="my-gcp-project, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "gce" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowedZones"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<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 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>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -8,6 +8,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
FontAwesomeSymbol,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@ -29,20 +31,6 @@ import { useToggle } from "@app/hooks";
|
||||
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCheck,
|
||||
faClock,
|
||||
faClose,
|
||||
faCodeBranch,
|
||||
faComment,
|
||||
faCopy,
|
||||
faEllipsis,
|
||||
faKey,
|
||||
faTag,
|
||||
faTags
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { memo, useEffect } from "react";
|
||||
@ -50,7 +38,12 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
SecretActionType,
|
||||
TFormSchema
|
||||
} from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
secret: DecryptedSecret;
|
||||
@ -206,7 +199,6 @@ export const SecretItem = memo(
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
@ -227,9 +219,12 @@ export const SecretItem = memo(
|
||||
onCheckedChange={() => onToggleSecretSelect(secret.id)}
|
||||
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faKey}
|
||||
className={twMerge("ml-3 block group-hover:hidden", isSelected && "hidden")}
|
||||
<FontAwesomeSymbol
|
||||
className={twMerge(
|
||||
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
||||
isSelected && "hidden"
|
||||
)}
|
||||
symbolName={FontAwesomeSpriteName.SecretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||
@ -278,10 +273,12 @@ export const SecretItem = memo(
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
@ -297,7 +294,14 @@ export const SecretItem = memo(
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} />
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3"
|
||||
symbolName={
|
||||
isSecValueCopied
|
||||
? FontAwesomeSpriteName.Check
|
||||
: FontAwesomeSpriteName.ClipboardCopy
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
@ -318,7 +322,10 @@ export const SecretItem = memo(
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeIcon icon={faTags} />
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
@ -334,7 +341,14 @@ export const SecretItem = memo(
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={`${secret.id}-${tagId}`}
|
||||
icon={isTagSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
icon={
|
||||
isTagSelected && (
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
)
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
@ -353,7 +367,12 @@ export const SecretItem = memo(
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
leftIcon={
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
@ -379,7 +398,10 @@ export const SecretItem = memo(
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCodeBranch} />
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@ -393,6 +415,7 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-reminder"
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
@ -404,9 +427,9 @@ export const SecretItem = memo(
|
||||
: "Reminder"
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
icon={faClock}
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Clock}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
@ -430,7 +453,10 @@ export const SecretItem = memo(
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<FontAwesomeIcon icon={faComment} />
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Comment}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
@ -466,10 +492,13 @@ export const SecretItem = memo(
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
className="p-0 opacity-0 group-hover:opacity-100 h-5 w-4"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsis} size="lg" />
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.More}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
@ -488,7 +517,10 @@ export const SecretItem = memo(
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-5 w-4"
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@ -516,10 +548,12 @@ export const SecretItem = memo(
|
||||
{isSubmitting ? (
|
||||
<Spinner className="m-0 h-4 w-4 p-0" />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
size="lg"
|
||||
className={twMerge("text-primary", errors.key && "text-mineshaft-300")}
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Check}
|
||||
className={twMerge(
|
||||
"h-4 w-4 text-primary",
|
||||
errors.key && "text-mineshaft-300"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
@ -536,7 +570,10 @@ export const SecretItem = memo(
|
||||
onClick={() => reset()}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Close}
|
||||
className="h-4 w-4 text-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@ -17,6 +18,7 @@ import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPa
|
||||
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
|
||||
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
||||
import { SecretItem } from "./SecretItem";
|
||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
secrets?: DecryptedSecret[];
|
||||
@ -89,7 +91,6 @@ export const SecretListView = ({
|
||||
isVisible,
|
||||
isProtectedBranch = false
|
||||
}: Props) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"deleteSecret",
|
||||
@ -341,6 +342,13 @@ export const SecretListView = ({
|
||||
>
|
||||
{namespace}
|
||||
</div>
|
||||
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
symbol={symbol}
|
||||
key={`font-awesome-svg-spritie-${symbol}`}
|
||||
/>
|
||||
))}
|
||||
{filteredSecrets.map((secret) => (
|
||||
<SecretItem
|
||||
environment={environment}
|
||||
|
@ -1,4 +1,16 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCheck,
|
||||
faClock,
|
||||
faClose,
|
||||
faCodeBranch,
|
||||
faComment,
|
||||
faCopy,
|
||||
faEllipsis,
|
||||
faKey,
|
||||
faTags
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { z } from "zod";
|
||||
|
||||
export enum SecretActionType {
|
||||
@ -41,3 +53,31 @@ export const formSchema = z.object({
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export enum FontAwesomeSpriteName {
|
||||
SecretKey = "secret-key",
|
||||
Check = "check",
|
||||
ClipboardCopy = "clipboard-copy",
|
||||
Tags = "secret-tags",
|
||||
Clock = "reminder-clock",
|
||||
Comment = "comment",
|
||||
More = "more",
|
||||
Override = "secret-override",
|
||||
Close = "close",
|
||||
CheckedCircle = "check-circle"
|
||||
}
|
||||
|
||||
// this is an optimization technique
|
||||
// https://docs.fontawesome.com/web/add-icons/svg-symbols
|
||||
export const FontAwesomeSpriteSymbols = [
|
||||
{ icon: faKey, symbol: FontAwesomeSpriteName.SecretKey },
|
||||
{ icon: faCheck, symbol: FontAwesomeSpriteName.Check },
|
||||
{ icon: faCopy, symbol: FontAwesomeSpriteName.ClipboardCopy },
|
||||
{ icon: faTags, symbol: FontAwesomeSpriteName.Tags },
|
||||
{ icon: faClock, symbol: FontAwesomeSpriteName.Clock },
|
||||
{ icon: faComment, symbol: FontAwesomeSpriteName.Comment },
|
||||
{ icon: faEllipsis, symbol: FontAwesomeSpriteName.More },
|
||||
{ icon: faCodeBranch, symbol: FontAwesomeSpriteName.Override },
|
||||
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
|
||||
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle }
|
||||
];
|
||||
|
Reference in New Issue
Block a user