diff --git a/backend/package-lock.json b/backend/package-lock.json
index 329913cd9..47e9014ce 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -31,7 +31,7 @@
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8",
- "@node-saml/passport-saml": "^4.0.4",
+ "@node-saml/passport-saml": "^5.0.1",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
@@ -6747,32 +6747,35 @@
}
},
"node_modules/@node-saml/node-saml": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz",
- "integrity": "sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.0.1.tgz",
+ "integrity": "sha512-YQzFPEC+CnsfO9AFYnwfYZKIzOLx3kITaC1HrjHVLTo6hxcQhc+LgHODOMvW4VCV95Gwrz1MshRUWCPzkDqmnA==",
+ "license": "MIT",
"dependencies": {
- "@types/debug": "^4.1.7",
- "@types/passport": "^1.0.11",
- "@types/xml-crypto": "^1.4.2",
- "@types/xml-encryption": "^1.2.1",
- "@types/xml2js": "^0.4.11",
- "@xmldom/xmldom": "^0.8.6",
+ "@types/debug": "^4.1.12",
+ "@types/qs": "^6.9.11",
+ "@types/xml-encryption": "^1.2.4",
+ "@types/xml2js": "^0.4.14",
+ "@xmldom/is-dom-node": "^1.0.1",
+ "@xmldom/xmldom": "^0.8.10",
"debug": "^4.3.4",
- "xml-crypto": "^3.0.1",
+ "xml-crypto": "^6.0.1",
"xml-encryption": "^3.0.2",
- "xml2js": "^0.5.0",
- "xmlbuilder": "^15.1.1"
+ "xml2js": "^0.6.2",
+ "xmlbuilder": "^15.1.1",
+ "xpath": "^0.0.34"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 18"
}
},
"node_modules/@node-saml/node-saml/node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -6783,25 +6786,43 @@
}
}
},
- "node_modules/@node-saml/node-saml/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/@node-saml/node-saml/node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/@node-saml/node-saml/node_modules/xml2js/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
},
"node_modules/@node-saml/passport-saml": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-4.0.4.tgz",
- "integrity": "sha512-xFw3gw0yo+K1mzlkW15NeBF7cVpRHN/4vpjmBKzov5YFImCWh/G0LcTZ8krH3yk2/eRPc3Or8LRPudVJBjmYaw==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-5.0.1.tgz",
+ "integrity": "sha512-fMztg3zfSnjLEgxvpl6HaDMNeh0xeQX4QHiF9e2Lsie2dc4qFE37XYbQZhVmn8XJ2awPpSWLQ736UskYgGU8lQ==",
+ "license": "MIT",
"dependencies": {
- "@node-saml/node-saml": "^4.0.4",
- "@types/express": "^4.17.14",
- "@types/passport": "^1.0.11",
- "@types/passport-strategy": "^0.2.35",
- "passport": "^0.6.0",
+ "@node-saml/node-saml": "^5.0.1",
+ "@types/express": "^4.17.21",
+ "@types/passport": "^1.0.16",
+ "@types/passport-strategy": "^0.2.38",
+ "passport": "^0.7.0",
"passport-strategy": "^1.0.0"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 18"
}
},
"node_modules/@nodelib/fs.scandir": {
@@ -9606,6 +9627,7 @@
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
"dependencies": {
"@types/ms": "*"
}
@@ -9725,9 +9747,10 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
},
"node_modules/@types/ms": {
- "version": "0.7.34",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
- "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
},
"node_modules/@types/node": {
"version": "20.9.5",
@@ -9907,9 +9930,10 @@
"dev": true
},
"node_modules/@types/qs": {
- "version": "6.9.10",
- "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
- "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw=="
+ "version": "6.9.18",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
+ "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
+ "license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
@@ -10058,19 +10082,11 @@
"@types/webidl-conversions": "*"
}
},
- "node_modules/@types/xml-crypto": {
- "version": "1.4.6",
- "resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.6.tgz",
- "integrity": "sha512-A6jEW2FxLZo1CXsRWnZHUX2wzR3uDju2Bozt6rDbSmU/W8gkilaVbwFEVN0/NhnUdMVzwYobWtM6bU1QJJFb7Q==",
- "dependencies": {
- "@types/node": "*",
- "xpath": "0.0.27"
- }
- },
"node_modules/@types/xml-encryption": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.4.tgz",
"integrity": "sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q==",
+ "license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -10079,6 +10095,7 @@
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
"integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
+ "license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -10522,10 +10539,20 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@xmldom/is-dom-node": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz",
+ "integrity": "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
+ "license": "MIT",
"engines": {
"node": ">=10.0.0"
}
@@ -18222,9 +18249,10 @@
}
},
"node_modules/passport": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
- "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
+ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
+ "license": "MIT",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
@@ -23692,42 +23720,44 @@
}
},
"node_modules/xml-crypto": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.2.0.tgz",
- "integrity": "sha512-qVurBUOQrmvlgmZqIVBqmb06TD2a/PpEUfFPgD7BuBfjmoH4zgkqaWSIJrnymlCvM2GGt9x+XtJFA+ttoAufqg==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-6.0.1.tgz",
+ "integrity": "sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==",
+ "license": "MIT",
"dependencies": {
- "@xmldom/xmldom": "^0.8.8",
- "xpath": "0.0.32"
+ "@xmldom/is-dom-node": "^1.0.1",
+ "@xmldom/xmldom": "^0.8.10",
+ "xpath": "^0.0.33"
},
"engines": {
- "node": ">=4.0.0"
+ "node": ">=16"
}
},
"node_modules/xml-crypto/node_modules/xpath": {
- "version": "0.0.32",
- "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
- "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz",
+ "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==",
+ "license": "MIT",
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/xml-encryption": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.0.2.tgz",
- "integrity": "sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.1.0.tgz",
+ "integrity": "sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q==",
+ "license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.8.5",
"escape-html": "^1.0.3",
"xpath": "0.0.32"
- },
- "engines": {
- "node": ">=12"
}
},
"node_modules/xml-encryption/node_modules/xpath": {
"version": "0.0.32",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz",
"integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==",
+ "license": "MIT",
"engines": {
"node": ">=0.6.0"
}
@@ -23764,6 +23794,7 @@
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "license": "MIT",
"engines": {
"node": ">=8.0"
}
@@ -23774,9 +23805,10 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"node_modules/xpath": {
- "version": "0.0.27",
- "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",
- "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==",
+ "version": "0.0.34",
+ "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz",
+ "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==",
+ "license": "MIT",
"engines": {
"node": ">=0.6.0"
}
diff --git a/backend/package.json b/backend/package.json
index 25380f896..c575722fd 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -148,7 +148,7 @@
"@fastify/swagger-ui": "^2.1.0",
"@google-cloud/kms": "^4.5.0",
"@infisical/quic": "^1.0.8",
- "@node-saml/passport-saml": "^4.0.4",
+ "@node-saml/passport-saml": "^5.0.1",
"@octokit/auth-app": "^7.1.1",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
diff --git a/backend/src/@types/fastify-request-context.d.ts b/backend/src/@types/fastify-request-context.d.ts
deleted file mode 100644
index fc8d94e07..000000000
--- a/backend/src/@types/fastify-request-context.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import "@fastify/request-context";
-
-declare module "@fastify/request-context" {
- interface RequestContextData {
- reqId: string;
- }
-}
diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 85a19b676..99c7c4184 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -100,6 +100,12 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
declare module "@fastify/request-context" {
interface RequestContextData {
reqId: string;
+ identityAuthInfo?: {
+ identityId: string;
+ oidc?: {
+ claims: Record;
+ };
+ };
}
}
diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts
index 7738da5cc..14c498ca9 100644
--- a/backend/src/db/migrations/20250212191958_create-gateway.ts
+++ b/backend/src/db/migrations/20250212191958_create-gateway.ts
@@ -85,7 +85,7 @@ export async function up(knex: Knex): Promise {
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
- const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
+ const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
// not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) {
diff --git a/backend/src/db/migrations/20250311105617_add-share-to-anyone-setting-to-organizations.ts b/backend/src/db/migrations/20250311105617_add-share-to-anyone-setting-to-organizations.ts
new file mode 100644
index 000000000..8f767c748
--- /dev/null
+++ b/backend/src/db/migrations/20250311105617_add-share-to-anyone-setting-to-organizations.ts
@@ -0,0 +1,32 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ if (await knex.schema.hasTable(TableName.Organization)) {
+ const hasSecretShareToAnyoneCol = await knex.schema.hasColumn(
+ TableName.Organization,
+ "allowSecretSharingOutsideOrganization"
+ );
+
+ if (!hasSecretShareToAnyoneCol) {
+ await knex.schema.alterTable(TableName.Organization, (t) => {
+ t.boolean("allowSecretSharingOutsideOrganization").defaultTo(true);
+ });
+ }
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ if (await knex.schema.hasTable(TableName.Organization)) {
+ const hasSecretShareToAnyoneCol = await knex.schema.hasColumn(
+ TableName.Organization,
+ "allowSecretSharingOutsideOrganization"
+ );
+ if (hasSecretShareToAnyoneCol) {
+ await knex.schema.alterTable(TableName.Organization, (t) => {
+ t.dropColumn("allowSecretSharingOutsideOrganization");
+ });
+ }
+ }
+}
diff --git a/backend/src/db/migrations/20250314145202_identity-oidc-claim-mapping.ts b/backend/src/db/migrations/20250314145202_identity-oidc-claim-mapping.ts
new file mode 100644
index 000000000..482c5c4eb
--- /dev/null
+++ b/backend/src/db/migrations/20250314145202_identity-oidc-claim-mapping.ts
@@ -0,0 +1,21 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
+ if (!hasMappingField) {
+ await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
+ t.jsonb("claimMetadataMapping");
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
+ if (hasMappingField) {
+ await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
+ t.dropColumn("claimMetadataMapping");
+ });
+ }
+}
diff --git a/backend/src/db/schemas/identity-oidc-auths.ts b/backend/src/db/schemas/identity-oidc-auths.ts
index ebde5e7dc..03bfcf40a 100644
--- a/backend/src/db/schemas/identity-oidc-auths.ts
+++ b/backend/src/db/schemas/identity-oidc-auths.ts
@@ -26,7 +26,8 @@ export const IdentityOidcAuthsSchema = z.object({
boundSubject: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
- encryptedCaCertificate: zodBuffer.nullable().optional()
+ encryptedCaCertificate: zodBuffer.nullable().optional(),
+ claimMetadataMapping: z.unknown().nullable().optional()
});
export type TIdentityOidcAuths = z.infer;
diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts
index 7169df59a..7e5994938 100644
--- a/backend/src/db/schemas/organizations.ts
+++ b/backend/src/db/schemas/organizations.ts
@@ -25,7 +25,8 @@ export const OrganizationsSchema = z.object({
selectedMfaMethod: z.string().nullable().optional(),
shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
- privilegeUpgradeInitiatedAt: z.date().nullable().optional()
+ privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
+ allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
});
export type TOrganizations = z.infer;
diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts
index 3406b4d63..24ea26677 100644
--- a/backend/src/db/schemas/secret-sharing.ts
+++ b/backend/src/db/schemas/secret-sharing.ts
@@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(),
- type: z.string(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),
@@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
lastViewedAt: z.date().nullable().optional(),
password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(),
- identifier: z.string().nullable().optional()
+ identifier: z.string().nullable().optional(),
+ type: z.string().default("share")
});
export type TSecretSharing = z.infer;
diff --git a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
index c19af4d22..b75a2dae0 100644
--- a/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
+++ b/backend/src/ee/routes/v1/dynamic-secret-lease-router.ts
@@ -1,10 +1,10 @@
-import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
+import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts
index 1d24c0578..b28d4b18d 100644
--- a/backend/src/ee/routes/v1/dynamic-secret-router.ts
+++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts
@@ -1,4 +1,3 @@
-import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
@@ -6,6 +5,7 @@ import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/pro
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
+import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
index 1eadb4051..d6b1a4c5c 100644
--- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
+++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts
@@ -1,11 +1,11 @@
import slugify from "@sindresorhus/slugify";
-import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts
index 45ab5044b..b7f5384ff 100644
--- a/backend/src/ee/routes/v1/kmip-router.ts
+++ b/backend/src/ee/routes/v1/kmip-router.ts
@@ -1,10 +1,10 @@
-import ms from "ms";
import { z } from "zod";
import { KmipClientsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
+import { ms } from "@app/lib/ms";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts
index 71facb22a..13ac5dfcb 100644
--- a/backend/src/ee/routes/v1/saml-router.ts
+++ b/backend/src/ee/routes/v1/saml-router.ts
@@ -25,7 +25,7 @@ type TSAMLConfig = {
callbackUrl: string;
entryPoint: string;
issuer: string;
- cert: string;
+ idpCert: string;
audience: string;
wantAuthnResponseSigned?: boolean;
wantAssertionsSigned?: boolean;
@@ -72,7 +72,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${ssoConfig.id}`,
entryPoint: ssoConfig.entryPoint,
issuer: ssoConfig.issuer,
- cert: ssoConfig.cert,
+ idpCert: ssoConfig.cert,
audience: appCfg.SITE_URL || ""
};
if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) {
@@ -302,15 +302,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const saml = await server.services.saml.createSamlCfg({
- actor: req.permission.type,
- actorId: req.permission.id,
- actorAuthMethod: req.permission.authMethod,
- actorOrgId: req.permission.orgId,
- orgId: req.body.organizationId,
- ...req.body
+ const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
+ const { permission } = req;
+
+ return server.services.saml.createSamlCfg({
+ isActive,
+ authProvider,
+ issuer,
+ entryPoint,
+ idpCert: cert,
+ actor: permission.type,
+ actorId: permission.id,
+ actorAuthMethod: permission.authMethod,
+ actorOrgId: permission.orgId,
+ orgId: req.body.organizationId
});
- return saml;
}
});
@@ -337,15 +343,21 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
- const saml = await server.services.saml.updateSamlCfg({
- actor: req.permission.type,
- actorId: req.permission.id,
- actorAuthMethod: req.permission.authMethod,
- actorOrgId: req.permission.orgId,
- orgId: req.body.organizationId,
- ...req.body
+ const { isActive, authProvider, issuer, entryPoint, cert } = req.body;
+ const { permission } = req;
+
+ return server.services.saml.updateSamlCfg({
+ isActive,
+ authProvider,
+ issuer,
+ entryPoint,
+ idpCert: cert,
+ actor: permission.type,
+ actorId: permission.id,
+ actorAuthMethod: permission.authMethod,
+ actorOrgId: permission.orgId,
+ orgId: req.body.organizationId
});
- return saml;
}
});
};
diff --git a/backend/src/ee/routes/v1/ssh-certificate-router.ts b/backend/src/ee/routes/v1/ssh-certificate-router.ts
index 5c135c3f1..a3ce95cb4 100644
--- a/backend/src/ee/routes/v1/ssh-certificate-router.ts
+++ b/backend/src/ee/routes/v1/ssh-certificate-router.ts
@@ -1,9 +1,9 @@
-import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
+import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
diff --git a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts
index f94b9d50a..a85e6b0ca 100644
--- a/backend/src/ee/routes/v1/ssh-certificate-template-router.ts
+++ b/backend/src/ee/routes/v1/ssh-certificate-template-router.ts
@@ -1,5 +1,4 @@
import slugify from "@sindresorhus/slugify";
-import ms from "ms";
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
@@ -10,6 +9,7 @@ import {
isValidUserPattern
} from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-validators";
import { SSH_CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
+import { ms } from "@app/lib/ms";
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";
diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts
index f8b4e922c..df8512ada 100644
--- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts
+++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts
@@ -1,11 +1,11 @@
import slugify from "@sindresorhus/slugify";
-import ms from "ms";
import { z } from "zod";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
+import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts
index 7cc9155cd..c8f0ba16c 100644
--- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts
+++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts
@@ -1,11 +1,11 @@
import slugify from "@sindresorhus/slugify";
-import ms from "ms";
import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
+import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
index 163b97256..29a151515 100644
--- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
+++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
@@ -1,9 +1,10 @@
import slugify from "@sindresorhus/slugify";
-import ms from "ms";
+import msFn from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -246,7 +247,7 @@ export const accessApprovalRequestServiceFactory = ({
requesterEmail: requestedByUser.email,
isTemporary,
...(isTemporary && {
- expiresIn: ms(ms(temporaryRange || ""), { long: true })
+ expiresIn: msFn(ms(temporaryRange || ""), { long: true })
}),
secretPath,
environment: envSlug,
diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts
index a8f96bb73..9244e569e 100644
--- a/backend/src/ee/services/audit-log/audit-log-types.ts
+++ b/backend/src/ee/services/audit-log/audit-log-types.ts
@@ -978,6 +978,7 @@ interface AddIdentityOidcAuthEvent {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record;
+ claimMetadataMapping: Record;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@@ -1002,6 +1003,7 @@ interface UpdateIdentityOidcAuthEvent {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record;
+ claimMetadataMapping?: Record;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts
index 39e8ae7e2..43f6197bf 100644
--- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts
+++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts
@@ -1,5 +1,4 @@
import { ForbiddenError, subject } from "@casl/ability";
-import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -11,6 +10,7 @@ import {
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
+import { ms } from "@app/lib/ms";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts
index aaecefb7f..32c32fbe3 100644
--- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts
+++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts
@@ -1,9 +1,9 @@
import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
-import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
index 35e26d942..be3d60e6a 100644
--- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
+++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts
@@ -1,9 +1,9 @@
import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
-import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts
index c7ae6b728..dc0a8e85d 100644
--- a/backend/src/ee/services/kmip/kmip-service.ts
+++ b/backend/src/ee/services/kmip/kmip-service.ts
@@ -1,11 +1,11 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
-import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidHostname, isValidIp } from "@app/lib/ip";
+import { ms } from "@app/lib/ms";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts
index 9d0f8c380..8b0292664 100644
--- a/backend/src/ee/services/permission/org-permission.ts
+++ b/backend/src/ee/services/permission/org-permission.ts
@@ -32,6 +32,10 @@ export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
+export enum OrgPermissionSecretShareAction {
+ ManageSettings = "manage-settings"
+}
+
export enum OrgPermissionGatewayActions {
// is there a better word for this. This mean can an identity be a gateway
CreateGateways = "create-gateways",
@@ -81,7 +85,8 @@ export enum OrgPermissionSubjects {
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip",
- Gateway = "gateway"
+ Gateway = "gateway",
+ SecretShare = "secret-share"
}
export type AppConnectionSubjectFields = {
@@ -113,7 +118,8 @@ export type OrgPermissionSet =
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
- | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
+ | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
+ | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
const AppConnectionConditionSchema = z
.object({
@@ -207,6 +213,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
"Describe what action an entity can take."
)
}),
+ z.object({
+ subject: z.literal(OrgPermissionSubjects.SecretShare).describe("The entity this permission pertains to."),
+ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSecretShareAction).describe(
+ "Describe what action an entity can take."
+ )
+ }),
z.object({
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
@@ -322,6 +334,8 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
+ can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
+
return rules;
};
diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts
index edec89983..753fff856 100644
--- a/backend/src/ee/services/permission/permission-fns.ts
+++ b/backend/src/ee/services/permission/permission-fns.ts
@@ -133,12 +133,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
}
}
-const escapeHandlebarsMissingMetadata = (obj: Record) => {
+const escapeHandlebarsMissingDict = (obj: Record, key: string) => {
const handler = {
get(target: Record, prop: string) {
- if (!(prop in target)) {
+ if (!Object.hasOwn(target, prop)) {
// eslint-disable-next-line no-param-reassign
- target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
+ target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
}
return target[prop];
}
@@ -194,7 +194,7 @@ const constructPermissionErrorMessage = (
export {
constructPermissionErrorMessage,
- escapeHandlebarsMissingMetadata,
+ escapeHandlebarsMissingDict,
isAuthMethodSaml,
validateOrgSSO,
validatePrivilegeChangeOperation
diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts
index 2997dcf74..4253c2440 100644
--- a/backend/src/ee/services/permission/permission-service.ts
+++ b/backend/src/ee/services/permission/permission-service.ts
@@ -1,5 +1,6 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
+import { requestContext } from "@fastify/request-context";
import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars";
@@ -22,7 +23,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
-import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
+import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
import {
TBuildOrgPermissionDTO,
TBuildProjectPermissionDTO,
@@ -243,20 +244,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
- const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
+ const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
- )
+ ),
+ "identity.metadata"
);
+ const templateValue = {
+ id: userProjectPermission.userId,
+ username: userProjectPermission.username,
+ metadata: metadataKeyValuePair
+ };
const interpolateRules = templatedRules(
{
- identity: {
- id: userProjectPermission.userId,
- username: userProjectPermission.username,
- metadata: metadataKeyValuePair
- }
+ identity: templateValue
},
{ data: false }
);
@@ -317,21 +320,26 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
- const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
- objectify(
- identityProjectPermission.metadata,
- (i) => i.key,
- (i) => i.value
- )
+ const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
+ const unescapedMetadata = objectify(
+ identityProjectPermission.metadata,
+ (i) => i.key,
+ (i) => i.value
);
-
+ const identityAuthInfo =
+ unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
+ ? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
+ : {};
+ const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
+ const templateValue = {
+ id: identityProjectPermission.identityId,
+ username: identityProjectPermission.username,
+ metadata: metadataKeyValuePair,
+ auth: identityAuthInfo
+ };
const interpolateRules = templatedRules(
{
- identity: {
- id: identityProjectPermission.identityId,
- username: identityProjectPermission.username,
- metadata: metadataKeyValuePair
- }
+ identity: templateValue
},
{ data: false }
);
@@ -429,20 +437,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
- const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
+ const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
- )
+ ),
+ "identity.metadata"
);
+ const templateValue = {
+ id: userProjectPermission.userId,
+ username: userProjectPermission.username,
+ metadata: metadataKeyValuePair
+ };
const interpolateRules = templatedRules(
{
- identity: {
- id: userProjectPermission.userId,
- username: userProjectPermission.username,
- metadata: metadataKeyValuePair
- }
+ identity: templateValue
},
{ data: false }
);
@@ -474,21 +484,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
- const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
+ const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
- )
+ ),
+ "identity.metadata"
);
-
+ const templateValue = {
+ id: identityProjectPermission.identityId,
+ username: identityProjectPermission.username,
+ metadata: metadataKeyValuePair
+ };
const interpolateRules = templatedRules(
{
- identity: {
- id: identityProjectPermission.identityId,
- username: identityProjectPermission.username,
- metadata: metadataKeyValuePair
- }
+ identity: templateValue
},
{ data: false }
);
diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
index cddc131c9..388786e82 100644
--- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
+++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts
@@ -1,9 +1,9 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
-import ms from "ms";
import { ActionProjectType, TableName } from "@app/db/schemas";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorType } from "@app/services/auth/auth-type";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts
index f22e2ad58..601347862 100644
--- a/backend/src/ee/services/saml-config/saml-config-service.ts
+++ b/backend/src/ee/services/saml-config/saml-config-service.ts
@@ -63,7 +63,7 @@ export const samlConfigServiceFactory = ({
kmsService
}: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({
- cert,
+ idpCert,
actor,
actorAuthMethod,
actorOrgId,
@@ -93,9 +93,9 @@ export const samlConfigServiceFactory = ({
orgId,
authProvider,
isActive,
- encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob,
+ encryptedSamlCertificate: encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob,
encryptedSamlEntryPoint: encryptor({ plainText: Buffer.from(entryPoint) }).cipherTextBlob,
- encryptedSamlCertificate: encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob
+ encryptedSamlIssuer: encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob
});
return samlConfig;
@@ -106,7 +106,7 @@ export const samlConfigServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
- cert,
+ idpCert,
actorId,
issuer,
isActive,
@@ -136,8 +136,8 @@ export const samlConfigServiceFactory = ({
updateQuery.encryptedSamlIssuer = encryptor({ plainText: Buffer.from(issuer) }).cipherTextBlob;
}
- if (cert !== undefined) {
- updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(cert) }).cipherTextBlob;
+ if (idpCert !== undefined) {
+ updateQuery.encryptedSamlCertificate = encryptor({ plainText: Buffer.from(idpCert) }).cipherTextBlob;
}
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts
index 03db4cfa1..444839a21 100644
--- a/backend/src/ee/services/saml-config/saml-config-types.ts
+++ b/backend/src/ee/services/saml-config/saml-config-types.ts
@@ -15,7 +15,7 @@ export type TCreateSamlCfgDTO = {
isActive: boolean;
entryPoint: string;
issuer: string;
- cert: string;
+ idpCert: string;
} & TOrgPermission;
export type TUpdateSamlCfgDTO = Partial<{
@@ -23,7 +23,7 @@ export type TUpdateSamlCfgDTO = Partial<{
isActive: boolean;
entryPoint: string;
issuer: string;
- cert: string;
+ idpCert: string;
}> &
TOrgPermission;
diff --git a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts
index 30c50cc9e..6687efaf9 100644
--- a/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts
+++ b/backend/src/ee/services/ssh-certificate-template/ssh-certificate-template-service.ts
@@ -1,10 +1,10 @@
import { ForbiddenError } from "@casl/ability";
-import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { TSshCertificateAuthorityDALFactory } from "../ssh/ssh-certificate-authority-dal";
import { TSshCertificateTemplateDALFactory } from "./ssh-certificate-template-dal";
diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
index f59a5d5bf..f51f8d639 100644
--- a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
+++ b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts
@@ -1,13 +1,13 @@
import { execFile } from "child_process";
import crypto from "crypto";
import { promises as fs } from "fs";
-import ms from "ms";
import os from "os";
import path from "path";
import { promisify } from "util";
import { TSshCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import {
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index fe893d5ab..8338492a4 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -329,6 +329,7 @@ export const OIDC_AUTH = {
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
boundAudiences: "The list of intended recipients.",
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
+ claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
boundSubject: "The expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.",
@@ -342,6 +343,7 @@ export const OIDC_AUTH = {
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
boundAudiences: "The new list of intended recipients.",
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
+ claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
boundSubject: "The new expected principal that is the subject of the JWT.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
@@ -1725,7 +1727,8 @@ export const SecretSyncs = {
SYNC_OPTIONS: (destination: SecretSync) => {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
- initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
+ initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
+ disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
};
},
ADDITIONAL_SYNC_OPTIONS: {
@@ -1771,6 +1774,12 @@ export const SecretSyncs = {
},
DATABRICKS: {
scope: "The Databricks secret scope that secrets should be synced to."
+ },
+ HUMANITEC: {
+ app: "The ID of the Humanitec app to sync secrets to.",
+ org: "The ID of the Humanitec org to sync secrets to.",
+ env: "The ID of the Humanitec environment to sync secrets to.",
+ scope: "The Humanitec scope that secrets should be synced to."
}
}
};
diff --git a/backend/src/lib/ms/index.ts b/backend/src/lib/ms/index.ts
new file mode 100644
index 000000000..5a0d165cd
--- /dev/null
+++ b/backend/src/lib/ms/index.ts
@@ -0,0 +1,15 @@
+import msFn, { StringValue } from "ms";
+
+import { BadRequestError } from "../errors";
+
+export const ms = (val: string) => {
+ if (typeof val !== "string") {
+ throw new BadRequestError({ message: `Date must be string` });
+ }
+
+ try {
+ return msFn(val as StringValue);
+ } catch {
+ throw new BadRequestError({ message: `Invalid date format string: ${val}` });
+ }
+};
diff --git a/backend/src/lib/template/dot-access.ts b/backend/src/lib/template/dot-access.ts
new file mode 100644
index 000000000..ec3208feb
--- /dev/null
+++ b/backend/src/lib/template/dot-access.ts
@@ -0,0 +1,34 @@
+/**
+ * Safely retrieves a value from a nested object using dot notation path
+ */
+export const getStringValueByDot = (
+ obj: Record | null | undefined,
+ path: string,
+ defaultValue?: string
+): string | undefined => {
+ // Handle null or undefined input
+ if (!obj) {
+ return defaultValue;
+ }
+
+ const parts = path.split(".");
+ let current: unknown = obj;
+
+ for (const part of parts) {
+ const isObject = typeof current === "object" && !Array.isArray(current) && current !== null;
+ if (!isObject) {
+ return defaultValue;
+ }
+ if (!Object.hasOwn(current as object, part)) {
+ // Check if the property exists as an own property
+ return defaultValue;
+ }
+ current = (current as Record)[part];
+ }
+
+ if (typeof current !== "string") {
+ return defaultValue;
+ }
+
+ return current;
+};
diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts
index 9d239a405..5779b39f7 100644
--- a/backend/src/server/plugins/auth/inject-identity.ts
+++ b/backend/src/server/plugins/auth/inject-identity.ts
@@ -1,3 +1,4 @@
+import { requestContext } from "@fastify/request-context";
import { FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import jwt, { JwtPayload } from "jsonwebtoken";
@@ -137,6 +138,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
identityName: identity.name,
authMethod: null
};
+ if (token?.identityAuth?.oidc) {
+ requestContext.set("identityAuthInfo", {
+ identityId: identity.identityId,
+ oidc: token?.identityAuth?.oidc
+ });
+ }
break;
}
case AuthMode.SERVICE_TOKEN: {
diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts
index 964a57a13..cf8181ec4 100644
--- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts
+++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts
@@ -18,6 +18,10 @@ import {
} from "@app/services/app-connection/databricks";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
+import {
+ HumanitecConnectionListItemSchema,
+ SanitizedHumanitecConnectionSchema
+} from "@app/services/app-connection/humanitec";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
@@ -27,7 +31,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedGcpConnectionSchema.options,
...SanitizedAzureKeyVaultConnectionSchema.options,
...SanitizedAzureAppConfigurationConnectionSchema.options,
- ...SanitizedDatabricksConnectionSchema.options
+ ...SanitizedDatabricksConnectionSchema.options,
+ ...SanitizedHumanitecConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -36,7 +41,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
GcpConnectionListItemSchema,
AzureKeyVaultConnectionListItemSchema,
AzureAppConfigurationConnectionListItemSchema,
- DatabricksConnectionListItemSchema
+ DatabricksConnectionListItemSchema,
+ HumanitecConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
diff --git a/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts
new file mode 100644
index 000000000..2d462c4ef
--- /dev/null
+++ b/backend/src/server/routes/v1/app-connection-routers/humanitec-connection-router.ts
@@ -0,0 +1,69 @@
+import z from "zod";
+
+import { readLimit } from "@app/server/config/rateLimiter";
+import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+import {
+ CreateHumanitecConnectionSchema,
+ HumanitecOrgWithApps,
+ SanitizedHumanitecConnectionSchema,
+ UpdateHumanitecConnectionSchema
+} from "@app/services/app-connection/humanitec";
+import { AuthMode } from "@app/services/auth/auth-type";
+
+import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
+
+export const registerHumanitecConnectionRouter = async (server: FastifyZodProvider) => {
+ registerAppConnectionEndpoints({
+ app: AppConnection.Humanitec,
+ server,
+ sanitizedResponseSchema: SanitizedHumanitecConnectionSchema,
+ createSchema: CreateHumanitecConnectionSchema,
+ updateSchema: UpdateHumanitecConnectionSchema
+ });
+
+ // The below endpoints are not exposed and for Infisical App use
+ server.route({
+ method: "GET",
+ url: `/:connectionId/organizations`,
+ config: {
+ rateLimit: readLimit
+ },
+ schema: {
+ params: z.object({
+ connectionId: z.string().uuid()
+ }),
+ response: {
+ 200: z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ apps: z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ envs: z
+ .object({
+ id: z.string(),
+ name: z.string()
+ })
+ .array()
+ })
+ .array()
+ })
+ .array()
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT]),
+ handler: async (req) => {
+ const { connectionId } = req.params;
+
+ const organizations: HumanitecOrgWithApps[] = await server.services.appConnection.humanitec.listOrganizations(
+ connectionId,
+ req.permission
+ );
+
+ return organizations;
+ }
+ });
+};
diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts
index e86c37753..c2b688a43 100644
--- a/backend/src/server/routes/v1/app-connection-routers/index.ts
+++ b/backend/src/server/routes/v1/app-connection-routers/index.ts
@@ -6,6 +6,7 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
+import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
export * from "./app-connection-router";
@@ -16,5 +17,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record,
+ claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
@@ -200,6 +203,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer),
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences),
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims),
+ claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(),
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject),
accessTokenTrustedIps: z
.object({
@@ -258,6 +262,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
boundIssuer: identityOidcAuth.boundIssuer,
boundAudiences: identityOidcAuth.boundAudiences,
boundClaims: identityOidcAuth.boundClaims as Record,
+ claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record,
boundSubject: identityOidcAuth.boundSubject as string,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index 84c0333f4..67b938ae4 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -256,7 +256,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
enforceMfa: z.boolean().optional(),
- selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
+ selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
+ allowSecretSharingOutsideOrganization: z.boolean().optional()
}),
response: {
200: z.object({
diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts
index 3c3a1ada4..b5a4a8677 100644
--- a/backend/src/server/routes/v1/project-membership-router.ts
+++ b/backend/src/server/routes/v1/project-membership-router.ts
@@ -1,4 +1,3 @@
-import ms from "ms";
import { z } from "zod";
import {
@@ -10,6 +9,7 @@ import {
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECT_USERS } from "@app/lib/api-docs";
+import { ms } from "@app/lib/ms";
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";
diff --git a/backend/src/server/routes/v1/secret-sync-routers/humanitec-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/humanitec-sync-router.ts
new file mode 100644
index 000000000..4ae64bfc4
--- /dev/null
+++ b/backend/src/server/routes/v1/secret-sync-routers/humanitec-sync-router.ts
@@ -0,0 +1,17 @@
+import {
+ CreateHumanitecSyncSchema,
+ HumanitecSyncSchema,
+ UpdateHumanitecSyncSchema
+} from "@app/services/secret-sync/humanitec";
+import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
+
+import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
+
+export const registerHumanitecSyncRouter = async (server: FastifyZodProvider) =>
+ registerSyncSecretsEndpoints({
+ destination: SecretSync.Humanitec,
+ server,
+ responseSchema: HumanitecSyncSchema,
+ createSchema: CreateHumanitecSyncSchema,
+ updateSchema: UpdateHumanitecSyncSchema
+ });
diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts
index 137335186..c342f3b73 100644
--- a/backend/src/server/routes/v1/secret-sync-routers/index.ts
+++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts
@@ -7,6 +7,7 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
+import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
export * from "./secret-sync-router";
@@ -17,5 +18,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record {
diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts
index aa85a8c40..47456e622 100644
--- a/backend/src/server/routes/v2/group-project-router.ts
+++ b/backend/src/server/routes/v2/group-project-router.ts
@@ -1,4 +1,3 @@
-import ms from "ms";
import { z } from "zod";
import {
@@ -8,6 +7,7 @@ import {
ProjectUserMembershipRolesSchema
} from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs";
+import { ms } from "@app/lib/ms";
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";
diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts
index b2cc3a8e9..18244068c 100644
--- a/backend/src/server/routes/v2/identity-project-router.ts
+++ b/backend/src/server/routes/v2/identity-project-router.ts
@@ -1,4 +1,3 @@
-import ms from "ms";
import { z } from "zod";
import {
@@ -9,6 +8,7 @@ import {
} from "@app/db/schemas";
import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts
index ce116a80a..9da622541 100644
--- a/backend/src/services/app-connection/app-connection-enums.ts
+++ b/backend/src/services/app-connection/app-connection-enums.ts
@@ -4,7 +4,8 @@ export enum AppConnection {
Databricks = "databricks",
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
- AzureAppConfiguration = "azure-app-configuration"
+ AzureAppConfiguration = "azure-app-configuration",
+ Humanitec = "humanitec"
}
export enum AWSRegion {
diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts
index c66fe52c4..fe5130ff1 100644
--- a/backend/src/services/app-connection/app-connection-fns.ts
+++ b/backend/src/services/app-connection/app-connection-fns.ts
@@ -35,6 +35,11 @@ import {
getAzureKeyVaultConnectionListItem,
validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
+import {
+ getHumanitecConnectionListItem,
+ HumanitecConnectionMethod,
+ validateHumanitecConnectionCredentials
+} from "./humanitec";
export const listAppConnectionOptions = () => {
return [
@@ -43,7 +48,8 @@ export const listAppConnectionOptions = () => {
getGcpConnectionListItem(),
getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem(),
- getDatabricksConnectionListItem()
+ getDatabricksConnectionListItem(),
+ getHumanitecConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -106,6 +112,8 @@ export const validateAppConnectionCredentials = async (
return validateAzureKeyVaultConnectionCredentials(appConnection);
case AppConnection.AzureAppConfiguration:
return validateAzureAppConfigurationConnectionCredentials(appConnection);
+ case AppConnection.Humanitec:
+ return validateHumanitecConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
@@ -128,6 +136,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Service Account Impersonation";
case DatabricksConnectionMethod.ServicePrincipal:
return "Service Principal";
+ case HumanitecConnectionMethod.API_TOKEN:
+ return "API Token";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts
index 8a045efe4..8a6c65426 100644
--- a/backend/src/services/app-connection/app-connection-maps.ts
+++ b/backend/src/services/app-connection/app-connection-maps.ts
@@ -6,5 +6,6 @@ export const APP_CONNECTION_NAME_MAP: Record = {
[AppConnection.GCP]: "GCP",
[AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
- [AppConnection.Databricks]: "Databricks"
+ [AppConnection.Databricks]: "Databricks",
+ [AppConnection.Humanitec]: "Humanitec"
};
diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts
index 8c4dd6a7c..e2e55bba0 100644
--- a/backend/src/services/app-connection/app-connection-service.ts
+++ b/backend/src/services/app-connection/app-connection-service.ts
@@ -35,6 +35,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
+import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
+import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@@ -50,7 +52,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record {
+ return {
+ name: "Humanitec" as const,
+ app: AppConnection.Humanitec as const,
+ methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.API_TOKEN]
+ };
+};
+
+export const validateHumanitecConnectionCredentials = async (config: THumanitecConnectionConfig) => {
+ const { credentials: inputCredentials } = config;
+
+ let response: AxiosResponse | null = null;
+
+ try {
+ response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, {
+ headers: {
+ Authorization: `Bearer ${inputCredentials.apiToken}`
+ }
+ });
+ } catch (error: unknown) {
+ if (error instanceof AxiosError) {
+ throw new BadRequestError({
+ message: `Failed to validate credentials: ${error.message || "Unknown error"}`
+ });
+ }
+ throw new BadRequestError({
+ message: "Unable to validate connection - verify credentials"
+ });
+ }
+
+ if (!response?.data) {
+ throw new InternalServerError({
+ message: "Failed to get organizations: Response was empty"
+ });
+ }
+
+ return inputCredentials;
+};
+
+export const listOrganizations = async (appConnection: THumanitecConnection): Promise => {
+ const {
+ credentials: { apiToken }
+ } = appConnection;
+ const response = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs`, {
+ headers: {
+ Authorization: `Bearer ${apiToken}`
+ }
+ });
+
+ if (!response.data) {
+ throw new InternalServerError({
+ message: "Failed to get organizations: Response was empty"
+ });
+ }
+ const orgs = response.data;
+ const orgsWithApps: HumanitecOrgWithApps[] = [];
+
+ for (const org of orgs) {
+ // eslint-disable-next-line no-await-in-loop
+ const appsResponse = await request.get(`${IntegrationUrls.HUMANITEC_API_URL}/orgs/${org.id}/apps`, {
+ headers: {
+ Authorization: `Bearer ${apiToken}`
+ }
+ });
+
+ if (appsResponse.data) {
+ const apps = appsResponse.data;
+ orgsWithApps.push({
+ ...org,
+ apps: apps.map((app) => ({
+ name: app.name,
+ id: app.id,
+ envs: app.envs
+ }))
+ });
+ }
+ }
+ return orgsWithApps;
+};
diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts
new file mode 100644
index 000000000..145f78b85
--- /dev/null
+++ b/backend/src/services/app-connection/humanitec/humanitec-connection-schemas.ts
@@ -0,0 +1,58 @@
+import z from "zod";
+
+import { AppConnections } from "@app/lib/api-docs";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+import {
+ BaseAppConnectionSchema,
+ GenericCreateAppConnectionFieldsSchema,
+ GenericUpdateAppConnectionFieldsSchema
+} from "@app/services/app-connection/app-connection-schemas";
+
+import { HumanitecConnectionMethod } from "./humanitec-connection-enums";
+
+export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({
+ apiToken: z.string().trim().min(1, "API Token required")
+});
+
+const BaseHumanitecConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Humanitec) });
+
+export const HumanitecConnectionSchema = BaseHumanitecConnectionSchema.extend({
+ method: z.literal(HumanitecConnectionMethod.API_TOKEN),
+ credentials: HumanitecConnectionAccessTokenCredentialsSchema
+});
+
+export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [
+ BaseHumanitecConnectionSchema.extend({
+ method: z.literal(HumanitecConnectionMethod.API_TOKEN),
+ credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({})
+ })
+]);
+
+export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [
+ z.object({
+ method: z
+ .literal(HumanitecConnectionMethod.API_TOKEN)
+ .describe(AppConnections?.CREATE(AppConnection.Humanitec).method),
+ credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe(
+ AppConnections.CREATE(AppConnection.Humanitec).credentials
+ )
+ })
+]);
+
+export const CreateHumanitecConnectionSchema = ValidateHumanitecConnectionCredentialsSchema.and(
+ GenericCreateAppConnectionFieldsSchema(AppConnection.Humanitec)
+);
+
+export const UpdateHumanitecConnectionSchema = z
+ .object({
+ credentials: HumanitecConnectionAccessTokenCredentialsSchema.optional().describe(
+ AppConnections.UPDATE(AppConnection.Humanitec).credentials
+ )
+ })
+ .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Humanitec));
+
+export const HumanitecConnectionListItemSchema = z.object({
+ name: z.literal("Humanitec"),
+ app: z.literal(AppConnection.Humanitec),
+ methods: z.nativeEnum(HumanitecConnectionMethod).array()
+});
diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts
new file mode 100644
index 000000000..5ade43450
--- /dev/null
+++ b/backend/src/services/app-connection/humanitec/humanitec-connection-service.ts
@@ -0,0 +1,29 @@
+import { logger } from "@app/lib/logger";
+import { OrgServiceActor } from "@app/lib/types";
+
+import { AppConnection } from "../app-connection-enums";
+import { listOrganizations as getHumanitecOrganizations } from "./humanitec-connection-fns";
+import { THumanitecConnection } from "./humanitec-connection-types";
+
+type TGetAppConnectionFunc = (
+ app: AppConnection,
+ connectionId: string,
+ actor: OrgServiceActor
+) => Promise;
+
+export const humanitecConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
+ const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
+ const appConnection = await getAppConnection(AppConnection.Humanitec, connectionId, actor);
+ try {
+ const organizations = await getHumanitecOrganizations(appConnection);
+ return organizations;
+ } catch (error) {
+ logger.error(error, "Failed to establish connection with Humanitec");
+ return [];
+ }
+ };
+
+ return {
+ listOrganizations
+ };
+};
diff --git a/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts b/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts
new file mode 100644
index 000000000..94613bfba
--- /dev/null
+++ b/backend/src/services/app-connection/humanitec/humanitec-connection-types.ts
@@ -0,0 +1,40 @@
+import z from "zod";
+
+import { DiscriminativePick } from "@app/lib/types";
+
+import { AppConnection } from "../app-connection-enums";
+import {
+ CreateHumanitecConnectionSchema,
+ HumanitecConnectionSchema,
+ ValidateHumanitecConnectionCredentialsSchema
+} from "./humanitec-connection-schemas";
+
+export type THumanitecConnection = z.infer;
+
+export type THumanitecConnectionInput = z.infer & {
+ app: AppConnection.Humanitec;
+};
+
+export type TValidateHumanitecConnectionCredentials = typeof ValidateHumanitecConnectionCredentialsSchema;
+
+export type THumanitecConnectionConfig = DiscriminativePick<
+ THumanitecConnectionInput,
+ "method" | "app" | "credentials"
+> & {
+ orgId: string;
+};
+
+export type HumanitecOrg = {
+ id: string;
+ name: string;
+};
+
+export type HumanitecApp = {
+ name: string;
+ id: string;
+ envs: { name: string; id: string }[];
+};
+
+export type HumanitecOrgWithApps = HumanitecOrg & {
+ apps: HumanitecApp[];
+};
diff --git a/backend/src/services/app-connection/humanitec/index.ts b/backend/src/services/app-connection/humanitec/index.ts
new file mode 100644
index 000000000..52fb6b3c2
--- /dev/null
+++ b/backend/src/services/app-connection/humanitec/index.ts
@@ -0,0 +1,4 @@
+export * from "./humanitec-connection-enums";
+export * from "./humanitec-connection-fns";
+export * from "./humanitec-connection-schemas";
+export * from "./humanitec-connection-types";
diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts
index 42efc4f29..3dfaa6532 100644
--- a/backend/src/services/certificate-authority/certificate-authority-service.ts
+++ b/backend/src/services/certificate-authority/certificate-authority-service.ts
@@ -2,7 +2,6 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
-import ms from "ms";
import { z } from "zod";
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
@@ -10,6 +9,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
diff --git a/backend/src/services/certificate-template/certificate-template-fns.ts b/backend/src/services/certificate-template/certificate-template-fns.ts
index 597be7eb2..0a19ac92f 100644
--- a/backend/src/services/certificate-template/certificate-template-fns.ts
+++ b/backend/src/services/certificate-template/certificate-template-fns.ts
@@ -1,7 +1,6 @@
-import ms from "ms";
-
import { TCertificateTemplates } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
+import { ms } from "@app/lib/ms";
export const validateCertificateDetailsAgainstTemplate = (
cert: {
diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts
index 05193cc92..3fface7e8 100644
--- a/backend/src/services/group-project/group-project-service.ts
+++ b/backend/src/services/group-project/group-project-service.ts
@@ -1,5 +1,4 @@
import { ForbiddenError } from "@casl/ability";
-import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import {
@@ -12,6 +11,7 @@ import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
+import { ms } from "@app/lib/ms";
import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts
index 86967df76..c97d2f40a 100644
--- a/backend/src/services/identity-access-token/identity-access-token-types.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-types.ts
@@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = {
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string;
+ identityAuth: {
+ oidc?: {
+ claims: Record;
+ };
+ };
};
diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts
index 114ca79c0..314db4ea5 100644
--- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts
+++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts
@@ -14,6 +14,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
+import { getStringValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -180,8 +181,9 @@ export const identityJwtAuthServiceFactory = ({
if (identityJwtAuth.boundClaims) {
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityJwtAuth.boundClaims as Record)[claimKey];
+ const value = getStringValueByDot(tokenData, claimKey) || "";
- if (!tokenData[claimKey]) {
+ if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts
index 3e58c8f6f..c22efa4d7 100644
--- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts
+++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts
@@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
+import { getStringValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
@@ -80,7 +81,7 @@ export const identityOidcAuthServiceFactory = ({
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
- httpsAgent: requestAgent
+ httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
}
);
const jwksUri = discoveryDoc.jwks_uri;
@@ -94,7 +95,7 @@ export const identityOidcAuthServiceFactory = ({
const client = new JwksClient({
jwksUri,
- requestAgent
+ requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
});
const { kid } = decodedToken.header;
@@ -111,7 +112,6 @@ export const identityOidcAuthServiceFactory = ({
message: `Access denied: ${error.message}`
});
}
-
throw error;
}
@@ -138,10 +138,16 @@ export const identityOidcAuthServiceFactory = ({
if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record)[claimKey];
+ const value = getStringValueByDot(tokenData, claimKey) || "";
+
+ if (!value) {
+ throw new UnauthorizedError({
+ message: `Access denied: token has no ${claimKey} field`
+ });
+ }
+
// handle both single and multi-valued claims
- if (
- !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
- ) {
+ if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed."
});
@@ -149,6 +155,20 @@ export const identityOidcAuthServiceFactory = ({
});
}
+ const filteredClaims: Record = {};
+ if (identityOidcAuth.claimMetadataMapping) {
+ Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
+ const claimKey = (identityOidcAuth.claimMetadataMapping as Record)[permissionKey];
+ const value = getStringValueByDot(tokenData, claimKey) || "";
+ if (!value) {
+ throw new UnauthorizedError({
+ message: `Access denied: token has no ${claimKey} field`
+ });
+ }
+ filteredClaims[permissionKey] = value;
+ });
+ }
+
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
@@ -170,7 +190,12 @@ export const identityOidcAuthServiceFactory = ({
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
- authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
+ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
+ identityAuth: {
+ oidc: {
+ claims: filteredClaims
+ }
+ }
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
@@ -191,6 +216,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@@ -257,6 +283,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
@@ -277,6 +304,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@@ -338,6 +366,7 @@ export const identityOidcAuthServiceFactory = ({
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject,
accessTokenMaxTTL,
accessTokenTTL,
diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts
index 761f68aa7..9727285e7 100644
--- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts
+++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts
@@ -7,6 +7,7 @@ export type TAttachOidcAuthDTO = {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record;
+ claimMetadataMapping?: Record;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@@ -21,6 +22,7 @@ export type TUpdateOidcAuthDTO = {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record;
+ claimMetadataMapping?: Record;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts
index 5486b37a8..398444553 100644
--- a/backend/src/services/identity-project/identity-project-service.ts
+++ b/backend/src/services/identity-project/identity-project-service.ts
@@ -1,5 +1,4 @@
import { ForbiddenError, subject } from "@casl/ability";
-import ms from "ms";
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
import {
@@ -10,6 +9,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
+import { ms } from "@app/lib/ms";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TProjectDALFactory } from "../project/project-dal";
diff --git a/backend/src/services/identity/identity-dal.ts b/backend/src/services/identity/identity-dal.ts
index 8b7fccab3..c4d0b6307 100644
--- a/backend/src/services/identity/identity-dal.ts
+++ b/backend/src/services/identity/identity-dal.ts
@@ -1,7 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName, TIdentities } from "@app/db/schemas";
-import { ormify, selectAllTableCols } from "@app/lib/knex";
import { DatabaseError } from "@app/lib/errors";
+import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TIdentityDALFactory = ReturnType;
diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts
index 16a717bd6..d6c450751 100644
--- a/backend/src/services/integration-auth/integration-list.ts
+++ b/backend/src/services/integration-auth/integration-list.ts
@@ -93,6 +93,7 @@ export enum IntegrationUrls {
NORTHFLANK_API_URL = "https://api.northflank.com",
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
AZURE_DEVOPS_API_URL = "https://dev.azure.com",
+ HUMANITEC_API_URL = "https://api.humanitec.io",
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index 9b4680317..e1592a654 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -22,6 +22,7 @@ import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import {
OrgPermissionActions,
OrgPermissionGroupActions,
+ OrgPermissionSecretShareAction,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import {
@@ -334,12 +335,27 @@ export const orgServiceFactory = ({
actorOrgId,
actorAuthMethod,
orgId,
- data: { name, slug, authEnforced, scimEnabled, defaultMembershipRoleSlug, enforceMfa, selectedMfaMethod }
+ data: {
+ name,
+ slug,
+ authEnforced,
+ scimEnabled,
+ defaultMembershipRoleSlug,
+ enforceMfa,
+ selectedMfaMethod,
+ allowSecretSharingOutsideOrganization
+ }
}: TUpdateOrgDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
+ if (allowSecretSharingOutsideOrganization !== undefined) {
+ ForbiddenError.from(permission).throwUnlessCan(
+ OrgPermissionSecretShareAction.ManageSettings,
+ OrgPermissionSubjects.SecretShare
+ );
+ }
const plan = await licenseService.getPlan(orgId);
const currentOrg = await orgDAL.findOrgById(actorOrgId);
@@ -406,7 +422,8 @@ export const orgServiceFactory = ({
scimEnabled,
defaultMembershipRole,
enforceMfa,
- selectedMfaMethod
+ selectedMfaMethod,
+ allowSecretSharingOutsideOrganization
});
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org;
diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts
index 28de72476..9d14b092e 100644
--- a/backend/src/services/org/org-types.ts
+++ b/backend/src/services/org/org-types.ts
@@ -72,6 +72,7 @@ export type TUpdateOrgDTO = {
defaultMembershipRoleSlug: string;
enforceMfa: boolean;
selectedMfaMethod: MfaMethod;
+ allowSecretSharingOutsideOrganization: boolean;
}>;
} & TOrgPermission;
diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts
index 2708e69ba..f45d9cc24 100644
--- a/backend/src/services/project-membership/project-membership-service.ts
+++ b/backend/src/services/project-membership/project-membership-service.ts
@@ -1,6 +1,5 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
-import ms from "ms";
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -14,6 +13,7 @@ import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/proj
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
+import { ms } from "@app/lib/ms";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts
index 1ce7acf44..9649be722 100644
--- a/backend/src/services/secret-sharing/secret-sharing-service.ts
+++ b/backend/src/services/secret-sharing/secret-sharing-service.ts
@@ -82,6 +82,13 @@ export const secretSharingServiceFactory = ({
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
$validateSharedSecretExpiry(expiresAt);
+ const org = await orgDAL.findOrgById(orgId);
+ if (!org.allowSecretSharingOutsideOrganization && accessType === SecretSharingAccessType.Anyone) {
+ throw new BadRequestError({
+ message: "Organization does not allow sharing secrets to members outside of this organization"
+ });
+ }
+
if (secretValue.length > 10_000) {
throw new BadRequestError({ message: "Shared secret value too long" });
}
diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts
index 9ca035774..7e77bd256 100644
--- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts
+++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts
@@ -382,6 +382,8 @@ export const AwsParameterStoreSyncFns = {
}
}
+ if (syncOptions.disableSecretDeletion) return;
+
const parametersToDelete: AWS.SSM.Parameter[] = [];
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts
index d4f272475..7cea12d1b 100644
--- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts
+++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts
@@ -396,6 +396,8 @@ export const AwsSecretsManagerSyncFns = {
}
}
+ if (syncOptions.disableSecretDeletion) return;
+
for await (const secretKey of Object.keys(awsSecretsRecord)) {
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
try {
diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts
index 8c0587599..64d82c125 100644
--- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts
+++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts
@@ -136,6 +136,8 @@ export const azureAppConfigurationSyncFactory = ({
}
}
+ if (secretSync.syncOptions.disableSecretDeletion) return;
+
for await (const key of Object.keys(azureAppConfigSecrets)) {
const azureSecret = azureAppConfigSecrets[key];
if (
diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts
index 21fef297a..b50d78bb2 100644
--- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts
+++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts
@@ -189,6 +189,8 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
});
}
+ if (secretSync.syncOptions.disableSecretDeletion) return;
+
for await (const deleteSecretKey of deleteSecrets.filter(
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret)
)) {
diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts
index 2d450b2b1..2ee7977a4 100644
--- a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts
+++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts
@@ -112,6 +112,8 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
accessToken
});
+ if (secretSync.syncOptions.disableSecretDeletion) return;
+
for await (const secret of databricksSecretKeys) {
if (!(secret.key in secretMap)) {
await deleteDatabricksSecrets({
diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
index f0eb3ce88..a71e29ae4 100644
--- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
+++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
@@ -71,8 +71,16 @@ const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCreden
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} catch (error) {
- // when a secret in GCP has no versions, we treat it as if it's a blank value
- if (error instanceof AxiosError && error.response?.status === 404) {
+ // when a secret in GCP has no versions, or is disabled/destroyed, we treat it as if it's a blank value
+ if (
+ error instanceof AxiosError &&
+ (error.response?.status === 404 ||
+ (error.response?.status === 400 &&
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ error.response.data.error.status === "FAILED_PRECONDITION" &&
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
+ error.response.data.error.message.match(/(?:disabled|destroyed)/i)))
+ ) {
res[key] = "";
} else {
throw new SecretSyncError({
@@ -147,6 +155,9 @@ export const GcpSyncFns = {
for await (const key of Object.keys(gcpSecrets)) {
try {
if (!(key in secretMap) || !secretMap[key].value) {
+ // eslint-disable-next-line no-continue
+ if (secretSync.syncOptions.disableSecretDeletion) continue;
+
// case: delete secret
await request.delete(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts
index a09a41163..1fe922de5 100644
--- a/backend/src/services/secret-sync/github/github-sync-fns.ts
+++ b/backend/src/services/secret-sync/github/github-sync-fns.ts
@@ -192,12 +192,6 @@ export const GithubSyncFns = {
const publicKey = await getPublicKey(client, secretSync);
- for await (const encryptedSecret of encryptedSecrets) {
- if (!(encryptedSecret.name in secretMap)) {
- await deleteSecret(client, secretSync, encryptedSecret);
- }
- }
-
await sodium.ready.then(async () => {
for await (const key of Object.keys(secretMap)) {
// convert secret & base64 key to Uint8Array.
@@ -224,6 +218,14 @@ export const GithubSyncFns = {
}
}
});
+
+ if (secretSync.syncOptions.disableSecretDeletion) return;
+
+ for await (const encryptedSecret of encryptedSecrets) {
+ if (!(encryptedSecret.name in secretMap)) {
+ await deleteSecret(client, secretSync, encryptedSecret);
+ }
+ }
},
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts
new file mode 100644
index 000000000..d81cfd041
--- /dev/null
+++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-constants.ts
@@ -0,0 +1,10 @@
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
+import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
+
+export const HUMANITEC_SYNC_LIST_OPTION: TSecretSyncListItem = {
+ name: "Humanitec",
+ destination: SecretSync.Humanitec,
+ connection: AppConnection.Humanitec,
+ canImportSecrets: false
+};
diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts
new file mode 100644
index 000000000..eb86fdf4f
--- /dev/null
+++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-enums.ts
@@ -0,0 +1,4 @@
+export enum HumanitecSyncScope {
+ Application = "application",
+ Environment = "environment"
+}
diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts
new file mode 100644
index 000000000..5fa0a3d63
--- /dev/null
+++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts
@@ -0,0 +1,220 @@
+import { request } from "@app/lib/config/request";
+import { logger } from "@app/lib/logger";
+import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
+import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
+import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
+import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
+
+import { HumanitecSyncScope } from "./humanitec-sync-enums";
+import { HumanitecSecret, THumanitecSyncWithCredentials } from "./humanitec-sync-types";
+
+const getHumanitecSecrets = async (secretSync: THumanitecSyncWithCredentials) => {
+ const {
+ destinationConfig,
+ connection: {
+ credentials: { apiToken }
+ }
+ } = secretSync;
+
+ let url = `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}`;
+ if (destinationConfig.scope === HumanitecSyncScope.Environment) {
+ url += `/envs/${destinationConfig.env}`;
+ }
+ url += "/values";
+
+ const { data } = await request.get(url, {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ });
+
+ return data;
+};
+
+const deleteSecret = async (secretSync: THumanitecSyncWithCredentials, encryptedSecret: HumanitecSecret) => {
+ const {
+ destinationConfig,
+ connection: {
+ credentials: { apiToken }
+ }
+ } = secretSync;
+
+ if (destinationConfig.scope === HumanitecSyncScope.Environment && encryptedSecret.source === "app") {
+ logger.info(
+ `Humanitec secret ${encryptedSecret.key} on app ${destinationConfig.app} has no environment override, not deleted as it is an app-level secret`
+ );
+ return;
+ }
+
+ try {
+ let url = `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}`;
+ if (destinationConfig.scope === HumanitecSyncScope.Environment) {
+ url += `/envs/${destinationConfig.env}`;
+ }
+ url += `/values/${encryptedSecret.key}`;
+
+ await request.delete(url, {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ });
+ } catch (error) {
+ throw new SecretSyncError({
+ error,
+ secretKey: encryptedSecret.key
+ });
+ }
+};
+
+const createSecret = async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap, key: string) => {
+ try {
+ const {
+ destinationConfig,
+ connection: {
+ credentials: { apiToken }
+ }
+ } = secretSync;
+
+ const appLevelSecret = destinationConfig.scope === HumanitecSyncScope.Application ? secretMap[key].value : "";
+ await request.post(
+ `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values`,
+ {
+ key,
+ value: appLevelSecret,
+ description: secretMap[key].comment || "",
+ is_secret: true
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ }
+ );
+ if (destinationConfig.scope === HumanitecSyncScope.Environment) {
+ await request.patch(
+ `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${key}`,
+ {
+ value: secretMap[key].value,
+ description: secretMap[key].comment || ""
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ }
+ );
+ }
+ } catch (error) {
+ throw new SecretSyncError({
+ error,
+ secretKey: key
+ });
+ }
+};
+
+const updateSecret = async (
+ secretSync: THumanitecSyncWithCredentials,
+ secretMap: TSecretMap,
+ encryptedSecret: HumanitecSecret
+) => {
+ try {
+ const {
+ destinationConfig,
+ connection: {
+ credentials: { apiToken }
+ }
+ } = secretSync;
+ if (destinationConfig.scope === HumanitecSyncScope.Application) {
+ await request.patch(
+ `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/values/${encryptedSecret.key}`,
+ {
+ value: secretMap[encryptedSecret.key].value,
+ description: secretMap[encryptedSecret.key].comment || ""
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ }
+ );
+ } else if (encryptedSecret.source === "app") {
+ await request.post(
+ `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values`,
+ {
+ value: secretMap[encryptedSecret.key].value,
+ description: secretMap[encryptedSecret.key].comment || "",
+ key: encryptedSecret.key,
+ is_secret: true
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ }
+ );
+ } else {
+ await request.patch(
+ `${IntegrationUrls.HUMANITEC_API_URL}/orgs/${destinationConfig.org}/apps/${destinationConfig.app}/envs/${destinationConfig.env}/values/${encryptedSecret.key}`,
+ {
+ value: secretMap[encryptedSecret.key].value,
+ description: secretMap[encryptedSecret.key].comment || ""
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ Accept: "application/json"
+ }
+ }
+ );
+ }
+ } catch (error) {
+ throw new SecretSyncError({
+ error,
+ secretKey: encryptedSecret.key
+ });
+ }
+};
+
+export const HumanitecSyncFns = {
+ syncSecrets: async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap) => {
+ const humanitecSecrets = await getHumanitecSecrets(secretSync);
+ const humanitecSecretsKeys = new Map(humanitecSecrets.map((s) => [s.key, s]));
+
+ for await (const key of Object.keys(secretMap)) {
+ const existingSecret = humanitecSecretsKeys.get(key);
+
+ if (!existingSecret) {
+ await createSecret(secretSync, secretMap, key);
+ } else {
+ await updateSecret(secretSync, secretMap, existingSecret);
+ }
+ }
+
+ if (secretSync.syncOptions.disableSecretDeletion) return;
+
+ for await (const humanitecSecret of humanitecSecrets) {
+ if (!secretMap[humanitecSecret.key]) {
+ await deleteSecret(secretSync, humanitecSecret);
+ }
+ }
+ },
+ getSecrets: async (secretSync: THumanitecSyncWithCredentials): Promise => {
+ throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
+ },
+
+ removeSecrets: async (secretSync: THumanitecSyncWithCredentials, secretMap: TSecretMap) => {
+ const encryptedSecrets = await getHumanitecSecrets(secretSync);
+
+ for await (const encryptedSecret of encryptedSecrets) {
+ if (encryptedSecret.key in secretMap) {
+ await deleteSecret(secretSync, encryptedSecret);
+ }
+ }
+ }
+};
diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts
new file mode 100644
index 000000000..cd90ecfdc
--- /dev/null
+++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-schemas.ts
@@ -0,0 +1,54 @@
+import { z } from "zod";
+
+import { SecretSyncs } from "@app/lib/api-docs";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
+import { HumanitecSyncScope } from "@app/services/secret-sync/humanitec/humanitec-sync-enums";
+import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
+import {
+ BaseSecretSyncSchema,
+ GenericCreateSecretSyncFieldsSchema,
+ GenericUpdateSecretSyncFieldsSchema
+} from "@app/services/secret-sync/secret-sync-schemas";
+import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
+
+const HumanitecSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
+ z.object({
+ scope: z.literal(HumanitecSyncScope.Application).describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.scope),
+ org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org),
+ app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app)
+ }),
+ z.object({
+ scope: z.literal(HumanitecSyncScope.Environment).describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.scope),
+ org: z.string().min(1, "Org ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.org),
+ app: z.string().min(1, "App ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.app),
+ env: z.string().min(1, "Env ID is required").describe(SecretSyncs.DESTINATION_CONFIG.HUMANITEC.env)
+ })
+]);
+
+const HumanitecSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
+
+export const HumanitecSyncSchema = BaseSecretSyncSchema(SecretSync.Humanitec, HumanitecSyncOptionsConfig).extend({
+ destination: z.literal(SecretSync.Humanitec),
+ destinationConfig: HumanitecSyncDestinationConfigSchema
+});
+
+export const CreateHumanitecSyncSchema = GenericCreateSecretSyncFieldsSchema(
+ SecretSync.Humanitec,
+ HumanitecSyncOptionsConfig
+).extend({
+ destinationConfig: HumanitecSyncDestinationConfigSchema
+});
+
+export const UpdateHumanitecSyncSchema = GenericUpdateSecretSyncFieldsSchema(
+ SecretSync.Humanitec,
+ HumanitecSyncOptionsConfig
+).extend({
+ destinationConfig: HumanitecSyncDestinationConfigSchema.optional()
+});
+
+export const HumanitecSyncListItemSchema = z.object({
+ name: z.literal("Humanitec"),
+ connection: z.literal(AppConnection.Humanitec),
+ destination: z.literal(SecretSync.Humanitec),
+ canImportSecrets: z.literal(false)
+});
diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts
new file mode 100644
index 000000000..d49e4401e
--- /dev/null
+++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-types.ts
@@ -0,0 +1,23 @@
+import z from "zod";
+
+import { THumanitecConnection } from "@app/services/app-connection/humanitec";
+
+import { CreateHumanitecSyncSchema, HumanitecSyncListItemSchema, HumanitecSyncSchema } from "./humanitec-sync-schemas";
+
+export type THumanitecSyncListItem = z.infer;
+
+export type THumanitecSync = z.infer;
+
+export type THumanitecSyncInput = z.infer;
+
+export type THumanitecSyncWithCredentials = THumanitecSync & {
+ connection: THumanitecConnection;
+};
+
+export type HumanitecSecret = {
+ description: string;
+ is_secret: boolean;
+ key: string;
+ source: "app" | "env";
+ value: string;
+};
diff --git a/backend/src/services/secret-sync/humanitec/index.ts b/backend/src/services/secret-sync/humanitec/index.ts
new file mode 100644
index 000000000..c1095fda0
--- /dev/null
+++ b/backend/src/services/secret-sync/humanitec/index.ts
@@ -0,0 +1,5 @@
+export * from "./humanitec-sync-constants";
+export * from "./humanitec-sync-enums";
+export * from "./humanitec-sync-fns";
+export * from "./humanitec-sync-schemas";
+export * from "./humanitec-sync-types";
diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts
index 743b900bc..006d033f5 100644
--- a/backend/src/services/secret-sync/secret-sync-enums.ts
+++ b/backend/src/services/secret-sync/secret-sync-enums.ts
@@ -5,7 +5,8 @@ export enum SecretSync {
GCPSecretManager = "gcp-secret-manager",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
- Databricks = "databricks"
+ Databricks = "databricks",
+ Humanitec = "humanitec"
}
export enum SecretSyncInitialSyncBehavior {
diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts
index 597c7fc01..6c8a6d4df 100644
--- a/backend/src/services/secret-sync/secret-sync-fns.ts
+++ b/backend/src/services/secret-sync/secret-sync-fns.ts
@@ -24,6 +24,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
+import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
+import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
const SECRET_SYNC_LIST_OPTIONS: Record = {
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -32,7 +34,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = {
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION,
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
- [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION
+ [SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION,
+ [SecretSync.Humanitec]: HUMANITEC_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -116,6 +119,8 @@ export const SecretSyncFns = {
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
+ case SecretSync.Humanitec:
+ return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -157,6 +162,9 @@ export const SecretSyncFns = {
appConnectionDAL,
kmsService
}).getSecrets(secretSync);
+ case SecretSync.Humanitec:
+ secretMap = await HumanitecSyncFns.getSecrets(secretSync);
+ break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -197,6 +205,8 @@ export const SecretSyncFns = {
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
+ case SecretSync.Humanitec:
+ return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts
index 92546706d..cd4125e1b 100644
--- a/backend/src/services/secret-sync/secret-sync-maps.ts
+++ b/backend/src/services/secret-sync/secret-sync-maps.ts
@@ -8,7 +8,8 @@ export const SECRET_SYNC_NAME_MAP: Record = {
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
[SecretSync.AzureKeyVault]: "Azure Key Vault",
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
- [SecretSync.Databricks]: "Databricks"
+ [SecretSync.Databricks]: "Databricks",
+ [SecretSync.Humanitec]: "Humanitec"
};
export const SECRET_SYNC_CONNECTION_MAP: Record = {
@@ -18,5 +19,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = {
[SecretSync.GCPSecretManager]: AppConnection.GCP,
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
- [SecretSync.Databricks]: AppConnection.Databricks
+ [SecretSync.Databricks]: AppConnection.Databricks,
+ [SecretSync.Humanitec]: AppConnection.Humanitec
};
diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts
index 8ea3a4f0b..50ff3f307 100644
--- a/backend/src/services/secret-sync/secret-sync-schemas.ts
+++ b/backend/src/services/secret-sync/secret-sync-schemas.ts
@@ -23,7 +23,8 @@ const BaseSyncOptionsSchema = ({
initialSyncBehavior: (canImportSecrets
? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
- ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior)
+ ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
+ disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
});
const schema = merge ? baseSchema.merge(merge) : baseSchema;
diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts
index 60742e797..2044c7c17 100644
--- a/backend/src/services/secret-sync/secret-sync-types.ts
+++ b/backend/src/services/secret-sync/secret-sync-types.ts
@@ -43,6 +43,12 @@ import {
TAzureKeyVaultSyncWithCredentials
} from "./azure-key-vault";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
+import {
+ THumanitecSync,
+ THumanitecSyncInput,
+ THumanitecSyncListItem,
+ THumanitecSyncWithCredentials
+} from "./humanitec";
export type TSecretSync =
| TAwsParameterStoreSync
@@ -51,7 +57,8 @@ export type TSecretSync =
| TGcpSync
| TAzureKeyVaultSync
| TAzureAppConfigurationSync
- | TDatabricksSync;
+ | TDatabricksSync
+ | THumanitecSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -60,7 +67,8 @@ export type TSecretSyncWithCredentials =
| TGcpSyncWithCredentials
| TAzureKeyVaultSyncWithCredentials
| TAzureAppConfigurationSyncWithCredentials
- | TDatabricksSyncWithCredentials;
+ | TDatabricksSyncWithCredentials
+ | THumanitecSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -69,7 +77,8 @@ export type TSecretSyncInput =
| TGcpSyncInput
| TAzureKeyVaultSyncInput
| TAzureAppConfigurationSyncInput
- | TDatabricksSyncInput;
+ | TDatabricksSyncInput
+ | THumanitecSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -78,7 +87,8 @@ export type TSecretSyncListItem =
| TGcpSyncListItem
| TAzureKeyVaultSyncListItem
| TAzureAppConfigurationSyncListItem
- | TDatabricksSyncListItem;
+ | TDatabricksSyncListItem
+ | THumanitecSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index bf10d67ab..1f343b75f 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
+import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
@@ -20,7 +21,6 @@ import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetIdentitiesDTO, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
-import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
type TSuperAdminServiceFactoryDep = {
identityDAL: Pick;
diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go
index c846db10d..8e9f5d63b 100644
--- a/cli/packages/cmd/secrets.go
+++ b/cli/packages/cmd/secrets.go
@@ -143,7 +143,15 @@ var secretsSetCmd = &cobra.Command{
Short: "Used set secrets",
Use: "set [secrets]",
DisableFlagsInUseLine: true,
- Args: cobra.MinimumNArgs(1),
+ Args: func(cmd *cobra.Command, args []string) error {
+ if cmd.Flags().Changed("file") {
+ if len(args) > 0 {
+ return fmt.Errorf("secrets cannot be provided as command-line arguments when the --file option is used. Please choose either file-based or argument-based secret input")
+ }
+ return nil
+ }
+ return cobra.MinimumNArgs(1)(cmd, args)
+ },
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
@@ -177,13 +185,18 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse secret type")
}
+ file, err := cmd.Flags().GetString("file")
+ if err != nil {
+ util.HandleError(err, "Unable to parse flag")
+ }
+
var secretOperations []models.SecretSetOperation
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
if projectId == "" {
util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag")
}
- secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token)
+ secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file)
} else {
if projectId == "" {
workspaceFile, err := util.GetWorkSpaceFromFile()
@@ -206,7 +219,7 @@ var secretsSetCmd = &cobra.Command{
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
Type: "",
Token: loggedInUserDetails.UserCredentials.JTWToken,
- })
+ }, file)
}
if err != nil {
@@ -691,6 +704,7 @@ func init() {
secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth")
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared")
+ secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.")
secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go
index 5a2a0ec24..02079d656 100644
--- a/cli/packages/util/secrets.go
+++ b/cli/packages/util/secrets.go
@@ -17,6 +17,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog/log"
"github.com/zalando/go-keyring"
+ "gopkg.in/yaml.v3"
)
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) ([]models.SingleEnvironmentVariable, error) {
@@ -564,7 +565,99 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
}
-func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) {
+func parseSecrets(fileName string, content string) (map[string]string, error) {
+ secrets := make(map[string]string)
+
+ if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") {
+ // Handle YAML secrets
+ var yamlData map[string]interface{}
+ if err := yaml.Unmarshal([]byte(content), &yamlData); err != nil {
+ return nil, fmt.Errorf("failed to parse YAML file: %v", err)
+ }
+
+ for key, value := range yamlData {
+ if strValue, ok := value.(string); ok {
+ secrets[key] = strValue
+ } else {
+ return nil, fmt.Errorf("YAML secret '%s' must be a string", key)
+ }
+ }
+ } else {
+ // Handle .env files
+ lines := strings.Split(content, "\n")
+
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+
+ // Ignore empty lines and comments
+ if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
+ continue
+ }
+
+ // Ensure it's a valid key=value pair
+ splitKeyValue := strings.SplitN(line, "=", 2)
+ if len(splitKeyValue) != 2 {
+ return nil, fmt.Errorf("invalid format, expected key=value in line: %s", line)
+ }
+
+ key, value := strings.TrimSpace(splitKeyValue[0]), strings.TrimSpace(splitKeyValue[1])
+
+ // Handle quoted values
+ if (strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`)) ||
+ (strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`)) {
+ value = value[1 : len(value)-1] // Remove surrounding quotes
+ }
+
+ secrets[key] = value
+ }
+ }
+
+ return secrets, nil
+}
+
+func validateSecretKey(key string) error {
+ if key == "" {
+ return errors.New("secret keys cannot be empty")
+ }
+ if unicode.IsNumber(rune(key[0])) {
+ return fmt.Errorf("secret key '%s' cannot start with a number", key)
+ }
+ if strings.Contains(key, " ") {
+ return fmt.Errorf("secret key '%s' cannot contain spaces", key)
+ }
+ return nil
+}
+
+func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) {
+ if file != "" {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ PrintErrorMessageAndExit("File does not exist")
+ }
+ return nil, fmt.Errorf("unable to process file [err=%v]", err)
+ }
+
+ parsedSecrets, err := parseSecrets(file, string(content))
+ if err != nil {
+ PrintErrorMessageAndExit(fmt.Sprintf("error parsing secrets: %v", err))
+ }
+
+ // Step 2: Validate secrets
+ for key, value := range parsedSecrets {
+ if err := validateSecretKey(key); err != nil {
+ PrintErrorMessageAndExit(err.Error())
+ }
+ if strings.TrimSpace(value) == "" {
+ PrintErrorMessageAndExit(fmt.Sprintf("Secret key '%s' has an empty value", key))
+ }
+ secretArgs = append(secretArgs, fmt.Sprintf("%s=%s", key, value))
+ }
+
+ if len(secretArgs) == 0 {
+ PrintErrorMessageAndExit("no valid secrets found in the file")
+ }
+ }
if tokenDetails == nil {
return nil, fmt.Errorf("unable to process set secret operations, token details are missing")
diff --git a/company/handbook/time-off.mdx b/company/handbook/time-off.mdx
index cce929fe4..de9c70697 100644
--- a/company/handbook/time-off.mdx
+++ b/company/handbook/time-off.mdx
@@ -15,15 +15,3 @@ Since Infisical's team is globally distributed, it is hard for us to keep track
## Winter break
Every year, Infisical team goes on a company-wide vacation during winter holidays. This year, the winter break period starts on December 21st, 2024 and ends on January 5th, 2025. You should expect to do no scheduled work during this period, but we will have a rotation process for [high and urgent service disruptions](https://infisical.com/sla).
-
-## Parental leave
-
-At Infisical, we recognize that parental leave is a special and important time, significantly different from a typical vacation. We’re proud to offer parental leave to everyone, regardless of gender, and whether you’ve become a parent through childbirth or adoption.
-
-For team members who have been with Infisical for over a year by the time of your child’s birth or adoption, you are eligible for up to 12 weeks of paid parental leave. This leave will be provided in one continuous block to allow you uninterrupted time with your family. If you have been with Infisical for less than a year, we will follow the parental leave provisions required by your local jurisdiction.
-
-While we trust your judgment, parental leave is intended to be a distinct benefit and is not designed to be combined with our unlimited PTO policy. To ensure fairness and balance, we generally discourage combining parental leave with an extended vacation.
-
-When you’re ready, please notify Maidul about your plans for parental leave, ideally at least four months in advance. This allows us to support you fully and arrange any necessary logistics, including salary adjustments and statutory paperwork.
-
-We’re here to support you as you embark on this exciting new chapter in your life!
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/available.mdx b/docs/api-reference/endpoints/app-connections/humanitec/available.mdx
new file mode 100644
index 000000000..eb95b2e54
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/available.mdx
@@ -0,0 +1,4 @@
+---
+title: "Available"
+openapi: "GET /api/v1/app-connections/humanitec/available"
+---
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/create.mdx b/docs/api-reference/endpoints/app-connections/humanitec/create.mdx
new file mode 100644
index 000000000..a4d196911
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/create.mdx
@@ -0,0 +1,4 @@
+---
+title: "Create"
+openapi: "POST /api/v1/app-connections/humanitec"
+---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx b/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx
new file mode 100644
index 000000000..d8786e08a
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v1/app-connections/humanitec/{connectionId}"
+---
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx
new file mode 100644
index 000000000..22473a7a1
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/get-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by ID"
+openapi: "GET /api/v1/app-connections/humanitec/{connectionId}"
+---
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx
new file mode 100644
index 000000000..fd848ae4f
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/get-by-name.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by Name"
+openapi: "GET /api/v1/app-connections/humanitec/connection-name/{connectionName}"
+---
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/list.mdx b/docs/api-reference/endpoints/app-connections/humanitec/list.mdx
new file mode 100644
index 000000000..07f30f674
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v1/app-connections/humanitec"
+---
diff --git a/docs/api-reference/endpoints/app-connections/humanitec/update.mdx b/docs/api-reference/endpoints/app-connections/humanitec/update.mdx
new file mode 100644
index 000000000..2a0806324
--- /dev/null
+++ b/docs/api-reference/endpoints/app-connections/humanitec/update.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/app-connections/humanitec/{connectionId}"
+---
\ No newline at end of file
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx
new file mode 100644
index 000000000..f683d67fe
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/create.mdx
@@ -0,0 +1,4 @@
+---
+title: "Create"
+openapi: "POST /api/v1/secret-syncs/humanitec"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx
new file mode 100644
index 000000000..ceef1fbb4
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/delete.mdx
@@ -0,0 +1,4 @@
+---
+title: "Delete"
+openapi: "DELETE /api/v1/secret-syncs/humanitec/{syncId}"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx
new file mode 100644
index 000000000..a8a2a9bfc
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-id.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by ID"
+openapi: "GET /api/v1/secret-syncs/humanitec/{syncId}"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx
new file mode 100644
index 000000000..ad2f11290
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/get-by-name.mdx
@@ -0,0 +1,4 @@
+---
+title: "Get by Name"
+openapi: "GET /api/v1/secret-syncs/humanitec/sync-name/{syncName}"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx
new file mode 100644
index 000000000..651e7a435
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/list.mdx
@@ -0,0 +1,4 @@
+---
+title: "List"
+openapi: "GET /api/v1/secret-syncs/humanitec"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx
new file mode 100644
index 000000000..7c5148638
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/remove-secrets.mdx
@@ -0,0 +1,4 @@
+---
+title: "Remove Secrets"
+openapi: "POST /api/v1/secret-syncs/humanitec/{syncId}/remove-secrets"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx
new file mode 100644
index 000000000..cb0446f11
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/sync-secrets.mdx
@@ -0,0 +1,4 @@
+---
+title: "Sync Secrets"
+openapi: "POST /api/v1/secret-syncs/humanitec/{syncId}/sync-secrets"
+---
diff --git a/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx b/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx
new file mode 100644
index 000000000..9e958555f
--- /dev/null
+++ b/docs/api-reference/endpoints/secret-syncs/humanitec/update.mdx
@@ -0,0 +1,4 @@
+---
+title: "Update"
+openapi: "PATCH /api/v1/secret-syncs/humanitec/{syncId}"
+---
diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx
index 2dff5cf7b..d1f4baa9f 100644
--- a/docs/cli/commands/secrets.mdx
+++ b/docs/cli/commands/secrets.mdx
@@ -219,6 +219,21 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
```
+
+ Used to set secrets from a file, supporting both `.env` and `YAML` formats. The file path can be either absolute or relative to the current working directory.
+
+ The file should contain secrets in the following formats:
+ - `key=value` for `.env` files
+ - `key: value` for YAML files
+
+ Comments can be written using `# comment` or `// comment`. Empty lines will be ignored during processing.
+
+
+ ```bash
+ # Example
+ infisical secrets set --file="./.env"
+ ```
+
diff --git a/docs/documentation/guides/local-development.mdx b/docs/documentation/guides/local-development.mdx
index 6d606bafe..2d4c21567 100644
--- a/docs/documentation/guides/local-development.mdx
+++ b/docs/documentation/guides/local-development.mdx
@@ -6,7 +6,7 @@ description: "Learn how to manage secrets in local development environments."
## Problem at hand
-There is a number of issues that arise with secret management in local development environment:
+There are a number of issues that arise with secret management in local development environment:
1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another.
2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time.
3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded secret as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability.
diff --git a/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png
new file mode 100644
index 000000000..e84a258e2
Binary files /dev/null and b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png differ
diff --git a/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png
new file mode 100644
index 000000000..824aabc25
Binary files /dev/null and b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png differ
diff --git a/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png
new file mode 100644
index 000000000..88d9a3da2
Binary files /dev/null and b/docs/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png differ
diff --git a/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx
new file mode 100644
index 000000000..5e1cc7093
--- /dev/null
+++ b/docs/documentation/platform/access-controls/abac/managing-machine-identity-attributes.mdx
@@ -0,0 +1,68 @@
+---
+title: "Machine identities"
+description: "Learn how to set metadata and leverage authentication attributes for machine identities."
+---
+
+Machine identities can have metadata set manually, just like users. In addition, during the machine authentication process (e.g., via OIDC), extra attributes called claims—are provided, which can be used in your ABAC policies.
+
+#### Setting Metadata on Machine Identities
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#### Accessing Attributes From Machine Identity Login
+
+When machine identities authenticate, they may receive additional payloads/attributes from the service provider.
+For methods like OIDC, these come as claims in the token and can be made available in your policies.
+
+
+
+ 1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
+ 2. In the **Advanced section**, locate the Claim Mapping configuration.
+ 3. Map the OIDC claims to permission attributes by specifying:
+ - **Attribute Name:** The identifier to be used in your policies (e.g., department).
+ - **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department).
+
+ For example, if your OIDC provider returns:
+
+ ```json
+ {
+ "sub": "machine456",
+ "name": "Service A",
+ "user": {
+ "department": "engineering",
+ "role": "service"
+ }
+ }
+ ```
+
+ You might map:
+
+ - **department:** to `user.department`
+ - **role:** to `user.role`
+
+ Once configured, these attributes become available in your policies using the following format:
+
+ ```
+ {{ identity.auth.oidc.claims. }}
+ ```
+
+
+
+
+ At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
+
+
diff --git a/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx
new file mode 100644
index 000000000..3f62a3b61
--- /dev/null
+++ b/docs/documentation/platform/access-controls/abac/managing-user-metadata.mdx
@@ -0,0 +1,39 @@
+---
+title: "Users identities"
+description: "How to set and use metadata attributes on user identities for ABAC."
+---
+
+User identities can have metadata attributes assigned directly. These attributes (such as location or department) are used to define dynamic access policies.
+
+#### Setting Metadata on Users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ For organizations using SAML for **user logins**, Infisical automatically maps metadata attributes from SAML assertions to user identities on every login. This enables dynamic policies based on the user's SAML attributes.
+
+
+
+#### Applying ABAC Policies with User Metadata
+Attribute-based access controls are currently only available for polices defined on Secrets Manager projects.
+You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags.
+
+
+
+In your policies, metadata values are accessed as follows:
+
+- **User ID:** `{{ identity.id }}` (always available)
+- **Username:** `{{ identity.username }}` (always available)
+- **Metadata Attributes:** `{{ identity.metadata. }}` (available if set)
diff --git a/docs/documentation/platform/access-controls/abac/overview.mdx b/docs/documentation/platform/access-controls/abac/overview.mdx
new file mode 100644
index 000000000..0a14719f6
--- /dev/null
+++ b/docs/documentation/platform/access-controls/abac/overview.mdx
@@ -0,0 +1,15 @@
+---
+title: "Overview"
+description: "Learn the basics of ABAC for both users and machine identities."
+---
+
+Infisical's Attribute-based Access Controls (ABAC) enable dynamic, attribute-driven permissions for both users and machine identities. ABAC enforces fine-grained, context-aware access controls using metadata attributes—stored as key-value pairs—either attached to identities or provided during authentication.
+
+
+
+ Manage user metadata manually or automatically via SAML logins.
+
+
+ Set metadata manually like users and access additional attributes provided during machine authentication (for example, OIDC claims).
+
+
\ No newline at end of file
diff --git a/docs/documentation/platform/access-controls/attribute-based-access-controls.mdx b/docs/documentation/platform/access-controls/attribute-based-access-controls.mdx
deleted file mode 100644
index 99c49c63a..000000000
--- a/docs/documentation/platform/access-controls/attribute-based-access-controls.mdx
+++ /dev/null
@@ -1,65 +0,0 @@
----
-title: "Attribute-based Access Controls"
-description: "Learn how to use ABAC to manage permissions based on identity attributes."
----
-
-Infisical's Attribute-based Access Controls (ABAC) allow for dynamic, attribute-driven permissions for both user and machine identities.
-ABAC policies use metadata attributes—stored as key-value pairs on identities—to enforce fine-grained permissions that are context aware.
-
-In ABAC, access controls are defined using metadata attributes, such as location or department, which can be set directly on user or machine identities.
-During policy execution, these attributes are evaluated, and determine whether said actor can access the requested resource or perform the requested operation.
-
-## Project-level Permissions
-
-Attribute-based access controls are currently available for polices defined on projects. You can set ABAC permissions to control access to environments, folders, secrets, and secret tags.
-
-### Setting Metadata on Identities
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- For organizations using SAML for login, Infisical automatically maps metadata attributes from SAML assertions to user identities.
- This makes it easy to create policies that dynamically adapt based on the SAML user’s attributes.
-
-
-
-
-## Defining ABAC Policies
-
-
-
-ABAC policies make use of identity metadata to define dynamic permissions. Each attribute must start and end with double curly-brackets `{{ }}`.
-The following attributes are available within project permissions:
-
-- **User ID**: `{{ identity.id }}`
-- **Username**: `{{ identity.username }}`
-- **Metadata Attributes**: `{{ identity.metadata. }}`
-
-During policy execution, these placeholders are replaced by their actual values prior to evaluation.
-
-### Example Use Case
-
-#### Location-based Access Control
-
-Suppose you want to restrict access to secrets within a specific folder based on a user's geographic region.
-You could assign a `location` attribute to each user (e.g., `identity.metadata.location`).
-You could then structure your folders to align with this attribute and define permissions accordingly.
-
-For example, a policy might restrict access to folders matching the user's location attribute in the following pattern:
-```
-/appA/{{ identity.metadata.location }}
-```
-Using this structure, users can only access folders that correspond to their configured `location` attribute.
-Consequently, if a users attribute changes due to relocation, no policies need to be changed to gain access to the folders associated with their new location.
diff --git a/docs/documentation/platform/access-controls/overview.mdx b/docs/documentation/platform/access-controls/overview.mdx
index 552117c7e..0afaaac54 100644
--- a/docs/documentation/platform/access-controls/overview.mdx
+++ b/docs/documentation/platform/access-controls/overview.mdx
@@ -18,7 +18,7 @@ To make sure that users and machine identities are only accessing the resources
diff --git a/docs/images/app-connections/humanitec/add-humanitec-connection.png b/docs/images/app-connections/humanitec/add-humanitec-connection.png
new file mode 100644
index 000000000..9ae9a7bf1
Binary files /dev/null and b/docs/images/app-connections/humanitec/add-humanitec-connection.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-add-api-token.png b/docs/images/app-connections/humanitec/humanitec-add-api-token.png
new file mode 100644
index 000000000..6eaf796c4
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-api-token.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-add-user-options.png b/docs/images/app-connections/humanitec/humanitec-add-user-options.png
new file mode 100644
index 000000000..ad6d28343
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user-options.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-add-user-role.png b/docs/images/app-connections/humanitec/humanitec-add-user-role.png
new file mode 100644
index 000000000..71f7647e6
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user-role.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-add-user.png b/docs/images/app-connections/humanitec/humanitec-add-user.png
new file mode 100644
index 000000000..d675c8336
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-add-user.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-created.png b/docs/images/app-connections/humanitec/humanitec-app-connection-created.png
new file mode 100644
index 000000000..0120adfb5
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-created.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png b/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png
new file mode 100644
index 000000000..9a96d6cee
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-modal.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-app-connection-option.png b/docs/images/app-connections/humanitec/humanitec-app-connection-option.png
new file mode 100644
index 000000000..1f1f724b2
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-app-connection-option.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-applications-tab.png b/docs/images/app-connections/humanitec/humanitec-applications-tab.png
new file mode 100644
index 000000000..c97bb96d9
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-applications-tab.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-connection.png b/docs/images/app-connections/humanitec/humanitec-connection.png
new file mode 100644
index 000000000..b18ac8aac
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-connection.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-copy-api-token.png b/docs/images/app-connections/humanitec/humanitec-copy-api-token.png
new file mode 100644
index 000000000..29f67954b
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-copy-api-token.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-create-api-token.png b/docs/images/app-connections/humanitec/humanitec-create-api-token.png
new file mode 100644
index 000000000..1cb198129
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-create-api-token.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-create-new-user.png b/docs/images/app-connections/humanitec/humanitec-create-new-user.png
new file mode 100644
index 000000000..f1ac7d029
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-create-new-user.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-service-account-filled.png b/docs/images/app-connections/humanitec/humanitec-service-account-filled.png
new file mode 100644
index 000000000..023253b88
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-service-account-filled.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-service-users.png b/docs/images/app-connections/humanitec/humanitec-service-users.png
new file mode 100644
index 000000000..3b95403ff
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-service-users.png differ
diff --git a/docs/images/app-connections/humanitec/humanitec-user-added.png b/docs/images/app-connections/humanitec/humanitec-user-added.png
new file mode 100644
index 000000000..fe918a7bd
Binary files /dev/null and b/docs/images/app-connections/humanitec/humanitec-user-added.png differ
diff --git a/docs/images/app-connections/humanitec/select-humanitec-connection.png b/docs/images/app-connections/humanitec/select-humanitec-connection.png
new file mode 100644
index 000000000..70d430cfc
Binary files /dev/null and b/docs/images/app-connections/humanitec/select-humanitec-connection.png differ
diff --git a/docs/images/platform/access-controls/abac-policies-by-auth.png b/docs/images/platform/access-controls/abac-policies-by-auth.png
new file mode 100644
index 000000000..3c75aabbe
Binary files /dev/null and b/docs/images/platform/access-controls/abac-policies-by-auth.png differ
diff --git a/docs/images/platform/access-controls/abac-policy-oidc-format.png b/docs/images/platform/access-controls/abac-policy-oidc-format.png
new file mode 100644
index 000000000..7fc76f50b
Binary files /dev/null and b/docs/images/platform/access-controls/abac-policy-oidc-format.png differ
diff --git a/docs/images/secret-syncs/humanitec/humanitec-created.png b/docs/images/secret-syncs/humanitec/humanitec-created.png
new file mode 100644
index 000000000..19e5d503a
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-created.png differ
diff --git a/docs/images/secret-syncs/humanitec/humanitec-destination.png b/docs/images/secret-syncs/humanitec/humanitec-destination.png
new file mode 100644
index 000000000..81605e9e2
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-destination.png differ
diff --git a/docs/images/secret-syncs/humanitec/humanitec-details.png b/docs/images/secret-syncs/humanitec/humanitec-details.png
new file mode 100644
index 000000000..172fde3f7
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-details.png differ
diff --git a/docs/images/secret-syncs/humanitec/humanitec-options.png b/docs/images/secret-syncs/humanitec/humanitec-options.png
new file mode 100644
index 000000000..bc907bbfc
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-options.png differ
diff --git a/docs/images/secret-syncs/humanitec/humanitec-review.png b/docs/images/secret-syncs/humanitec/humanitec-review.png
new file mode 100644
index 000000000..2ffda9240
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-review.png differ
diff --git a/docs/images/secret-syncs/humanitec/humanitec-source.png b/docs/images/secret-syncs/humanitec/humanitec-source.png
new file mode 100644
index 000000000..ff50c11d8
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/humanitec-source.png differ
diff --git a/docs/images/secret-syncs/humanitec/select-humanitec-option.png b/docs/images/secret-syncs/humanitec/select-humanitec-option.png
new file mode 100644
index 000000000..bb0cb9aed
Binary files /dev/null and b/docs/images/secret-syncs/humanitec/select-humanitec-option.png differ
diff --git a/docs/integrations/app-connections/humanitec.mdx b/docs/integrations/app-connections/humanitec.mdx
new file mode 100644
index 000000000..570d3ba5d
--- /dev/null
+++ b/docs/integrations/app-connections/humanitec.mdx
@@ -0,0 +1,71 @@
+---
+title: "Humanitec Connection"
+description: "Learn how to configure a Humanitec Connection for Infisical."
+---
+
+Infisical supports connecting to Humanitec using a service user.
+
+## Setup Humanitec Connection in Infisical
+
+
+
+ Navigate to the Humanitec **Service Users** tab.
+ 
+
+
+ Create a new service user. Take into account that the role set here will affect the permissions of the API Token so be sure to set it so the Service User has access permissions to the App you want to integrate to Infisical.
+ 
+
+
+ Add a new API token for the service user.
+ 
+
+
+ Create the API token for the service user.
+ This token's permission will be limited to the **Service User** role.
+
+ If you configure an expiry date for your API token you will need to manually rotate to a new token prior to expiration to avoid integration downtime.
+
+ 
+
+
+ A modal with the API token will be displayed. Save the token in a secure location for later use in the following steps.
+ 
+
+
+ After following the previous steps the Service User has been successfully created, and now should be visible on the Service Users tab.
+ 
+
+
+ Move to the **Applications** tab and add the Service User to the Application you want to sync with Infisical.
+ Clicking on the App Title will open the App details page.
+ 
+
+
+ Move to the **People** tab and add a new member to this Application. The recently created User Service should be visible on the dropdown shown.
+ Make sure to assign at least Developer role as Write permissions are required.
+ 
+ 
+ 
+
+
+ Your **Humanitec Connection** is now available for use.
+ 
+
+
+ Navigate to the **App Connections** tab on the **Organization Settings** page.
+ 
+
+
+ Select the **Humanitec Connection** option from the connection options modal.
+ 
+
+
+ Fill the Humanitec Connection modal, here you will need to provide the User Service API Token generated in the previous step.
+ 
+
+
+ Your **Humanitec Connection** is now available for use.
+ 
+
+
diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx
index 7fc21e8f9..6e99d4e13 100644
--- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx
+++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx
@@ -137,10 +137,8 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret.
-
-
-
+
The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`.
diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx
index b36495688..3266fd2b0 100644
--- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx
+++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx
@@ -93,7 +93,7 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
CA certificate to use for connecting to the Infisical instance with SSL/TLS.
-### Authentication methods
+### Authentication Methods
To retrieve the requested secrets, the operator must first authenticate with Infisical.
The list of available authentication methods are shown below.
@@ -535,7 +535,7 @@ spec:
-### Operator managed secrets
+### Operator Managed Secrets
The managed secret properties specify where to store the secrets retrieved from your Infisical project.
This includes defining the name and namespace of the Kubernetes secret that will hold these secrets.
@@ -584,7 +584,7 @@ This is useful for tools such as ArgoCD, where every resource requires an owner
-### Manged secret templating
+#### Managed Secret Templating
Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful.
Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Secrets.
@@ -681,6 +681,135 @@ template:
+### Operator Managed ConfigMaps
+
+The managed config map properties specify where to store the secrets retrieved from your Infisical project. Config maps can be used to store **non-sensitive** data, such as application configuration variables.
+The properties includes defining the name and namespace of the Kubernetes config map that will hold the data retrieved from your Infisical project.
+The Infisical operator will automatically create the Kubernetes config map in the specified name/namespace and ensure it stays up-to-date. If a config map already exists in the specified namespace, the operator will update the existing config map with the new data.
+
+
+ The usage of config maps is only intended for storing non-sensitive data. If you are looking to store sensitive data, please use the [managed secret](#operator-managed-secrets) property instead.
+
+
+
+
+
+ The name of the managed Kubernetes config map that your Infisical data will be stored in.
+
+
+ The namespace of the managed Kubernetes config map that your Infisical data will be stored in.
+
+
+ Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator.
+ This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
+
+ #### Available options
+
+ - `Orphan` (default)
+ - `Owner`
+
+
+ When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in
+ the same namespace as where the managed kubernetes config map.
+
+
+
+
+
+#### Managed ConfigMap Templating
+
+Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful.
+Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Config Maps.
+
+
+
+
+ This property controls what secrets are included in your managed config map when using templates.
+ When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes config map resource.
+ **Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.**
+
+ When set to `false`, only secrets defined in the `managedKubeConfigMapReferences[].template.data` field of the template will be included in the managed config map.
+ Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes.
+
+
+
+ Define secret keys and their corresponding templates.
+ Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
+
+ Secrets are structured as follows:
+
+ ```golang
+ type TemplateSecret struct {
+ Value string `json:"value"`
+ SecretPath string `json:"secretPath"`
+ }
+ ```
+
+ #### Example template configuration:
+
+ ```yaml
+ managedKubeConfigMapReferences:
+ - configMapName: managed-configmap
+ configMapNamespace: default
+ template:
+ includeAllSecrets: true
+ data:
+ # Create new key that doesn't exist in your Infisical project using values of other secrets
+ SITE_URL: "{{ .SITE_URL.Value }}"
+ # Override an existing key in Infisical project with a new value using values of other secrets
+ API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com"
+ ```
+
+ For this example, let's assume the following secrets exist in your Infisical project:
+
+ ```
+ SITE_URL = "https://example.com"
+ REGION = "us-east-1"
+ API_URL = "old-url" # This will be overridden
+ ```
+
+ The resulting managed Kubernetes config map will then contain:
+
+ ```
+ # Original config map data (from includeAllSecrets: true)
+ SITE_URL = "https://example.com"
+ REGION = "us-east-1"
+
+ # New and overridden config map data
+ SITE_URL = "https://example.com"
+ API_URL = "https://api.example.com.us-east-1.com" # Existing secret overridden by template
+ ```
+
+ To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates.
+
+ ### Available templating functions
+
+
+ **Function name**: decodeBase64ToBytes
+
+ **Description**:
+ Given a base64 encoded string, this function will decodes the base64-encoded string.
+ This function is useful when your Infisical secrets are already stored as base64 encoded value in Infisical.
+
+ **Returns**: The decoded base64 string as bytes.
+
+ **Example**:
+ The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical.
+ The resulting managed config map will contain the decoded value of `BINARY_KEY_BASE64`.
+
+ ```yaml
+ managedKubeConfigMapReferences:
+ - configMapName: managed-configmap
+ configMapNamespace: default
+ template:
+ includeAllSecrets: true
+ data:
+ BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
+ ```
+
+
+
+
## Applying CRD
Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster.
@@ -692,17 +821,32 @@ kubectl apply -f example-infisical-secret-crd.yaml
To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified.
-```bash
-# Verify managed secret is created
-kubectl get secrets -n
-```
+
+
+ ```bash
+ # Verify managed secret is created
+ kubectl get secrets -n
+ ```
+
+ The Infisical secrets will be synced and stored into the managed secret every
+ 1 minute unless configured otherwise.
+
+
+
+ ```bash
+ # Verify managed config map is created
+ kubectl get configmaps -n
+ ```
+
+ The Infisical config map data will be synced and stored into the managed config map every
+ 1 minute unless configured otherwise.
+
+
+
-
- The Infisical secrets will be synced and stored into the managed secret every
- 1 minutes.
-
-## Using managed secret in your deployment
+
+## Using Managed Secret In Your Deployment
To make use of the managed secret created by the operator into your deployment can be achieved through several methods.
Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/)
@@ -755,7 +899,7 @@ spec:
valueFrom:
secretKeyRef:
name: managed-secret # managed secret name
- key: SOME_SECRET_KEY # The name of the key which exists in the managed secret
+ key: SOME_SECRET_KEY # The name of the key which exists in the managed secret
```
Example usage in a deployment
@@ -764,28 +908,30 @@ Example usage in a deployment
apiVersion: apps/v1
kind: Deployment
metadata:
-name: nginx-deployment
-labels:
-app: nginx
+ name: nginx-deployment
+ labels:
+ app: nginx
spec:
-replicas: 1
-selector:
-matchLabels:
-app: nginx
-template:
-metadata:
-labels:
-app: nginx
-spec:
-containers: - name: nginx
-image: nginx:1.14.2
-env: - name: STRIPE_API_SECRET
-valueFrom:
-secretKeyRef:
-name: managed-secret # <- name of managed secret
-key: STRIPE_API_SECRET
-ports: - containerPort: 80
-
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ env:
+ - name: STRIPE_API_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: managed-secret # <- name of managed secret
+ key: STRIPE_API_SECRET
+ ports:
+ - containerPort: 80
```
@@ -861,12 +1007,12 @@ stringData:
-----END CERTIFICATE-----
```
-### Auto redeployment
+### Automatic Redeployment
Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed.
To address this, we added functionality to automatically redeploy your deployment when its managed secret updates.
-#### Enabling auto redeploy
+#### Enabling Automatic Redeployment
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
@@ -910,7 +1056,173 @@ spec:
Then, for each deployment that has this annotation present, a rolling update will be triggered.
-## Propagating labels & annotations
+## Using Managed ConfigMap In Your Deployment
+
+To make use of the managed ConfigMap created by the operator into your deployment can be achieved through several methods.
+Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes ConfigMaps [here](https://kubernetes.io/docs/concepts/configuration/configmap/)
+
+
+ Automatic redeployment of deployments using managed ConfigMaps is not yet supported.
+
+
+
+
+ This will take all the secrets from your managed ConfigMap and expose them to your container
+
+````yaml
+ envFrom:
+ - configMapRef:
+ name: managed-configmap # managed configmap name
+ ```
+
+ Example usage in a deployment
+ ```yaml
+ apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx-deployment
+ labels:
+ app: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ envFrom:
+ - configMapRef:
+ name: managed-configmap # <- name of managed configmap
+ ports:
+ - containerPort: 80
+````
+
+
+
+
+ This will allow you to select individual secrets by key name from your managed ConfigMap and expose them to your container
+
+ ```yaml
+ env:
+ - name: CONFIG_NAME # The environment variable's name which is made available in the container
+ valueFrom:
+ configMapKeyRef:
+ name: managed-configmap # managed configmap name
+ key: SOME_CONFIG_KEY # The name of the key which exists in the managed configmap
+ ```
+
+Example usage in a deployment
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx-deployment
+ labels:
+ app: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ env:
+ - name: STRIPE_API_SECRET
+ valueFrom:
+ configMapKeyRef:
+ name: managed-configmap # <- name of managed configmap
+ key: STRIPE_API_SECRET
+ ports:
+ - containerPort: 80
+```
+
+
+
+
+This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret
+```yaml
+volumes:
+ - name: configmaps-volume-name # The name of the volume under which configmaps will be stored
+ configMap:
+ name: managed-configmap # managed configmap name
+````
+
+You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets
+
+```yaml
+volumeMounts:
+ - name: configmaps-volume-name
+ mountPath: /etc/config
+ readOnly: true
+```
+
+Example usage in a deployment
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx-deployment
+ labels:
+ app: nginx
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ volumeMounts:
+ - name: configmaps-volume-name
+ mountPath: /etc/config
+ readOnly: true
+ ports:
+ - containerPort: 80
+ volumes:
+ - name: configmaps-volume-name
+ configMap:
+ name: managed-configmap # <- managed configmap
+```
+
+
+
+The definition file of the Kubernetes secret for the CA certificate can be structured like the following:
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: custom-ca-certificate
+type: Opaque
+stringData:
+ ca.crt: |
+ -----BEGIN CERTIFICATE-----
+ MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
+ ...
+ BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
+ -----END CERTIFICATE-----
+```
+
+## Propagating Labels & Annotations
The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created.
Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example:
@@ -949,5 +1261,4 @@ metadata:
namespace: default
type: Opaque
```
-
diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx
index 165998841..fad37265a 100644
--- a/docs/integrations/secret-syncs/aws-parameter-store.mdx
+++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx
@@ -43,8 +43,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
- **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with.
- **Tags**: Optional resource tags to add to parameters synced by Infisical.
- **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical.
- Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
+
+ Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
+
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Parameter Store Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx
index b8df0e8b3..8ed85be25 100644
--- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx
+++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx
@@ -46,7 +46,11 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical."
- **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with.
- **Tags**: Optional tags to add to secrets synced by Infisical.
- **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical.
+
+ Manually configured tags from the **Tags** field will take precedence over secret metadata when tag keys conflict.
+
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Secrets Manager Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx
index 3b443c672..35a577872 100644
--- a/docs/integrations/secret-syncs/azure-app-configuration.mdx
+++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx
@@ -6,7 +6,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
**Prerequisites:**
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure App Configuration.
+ - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration)
The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal
@@ -50,6 +50,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic
- **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Azure App Configuration Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx
index 9b28f841f..5f55a73ae 100644
--- a/docs/integrations/secret-syncs/azure-key-vault.mdx
+++ b/docs/integrations/secret-syncs/azure-key-vault.mdx
@@ -6,7 +6,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
**Prerequisites:**
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
- - Create a [Azure Connection](/integrations/app-connections/azure), configured for Azure Key Vault.
+ - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault)
The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal
@@ -52,6 +52,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical."
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict.
- **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Azure Key Vault Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx
index 148542708..c9db5f88a 100644
--- a/docs/integrations/secret-syncs/databricks.mdx
+++ b/docs/integrations/secret-syncs/databricks.mdx
@@ -47,6 +47,7 @@ description: "Learn how to configure a Databricks Sync for Infisical."
Databricks does not support importing secrets.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your Databricks Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx
index bcb3b87bd..72c932116 100644
--- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx
+++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx
@@ -43,6 +43,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict.
- **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your GCP Secret Manager Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx
index d7d7afd86..d55ec3d0b 100644
--- a/docs/integrations/secret-syncs/github.mdx
+++ b/docs/integrations/secret-syncs/github.mdx
@@ -63,6 +63,7 @@ description: "Learn how to configure a GitHub Sync for Infisical."
GitHub does not support importing secrets.
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
6. Configure the **Details** of your GitHub Sync, then click **Next**.

diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx
new file mode 100644
index 000000000..e8cd7eafc
--- /dev/null
+++ b/docs/integrations/secret-syncs/humanitec.mdx
@@ -0,0 +1,157 @@
+---
+title: "Humanitec Sync"
+description: "Learn how to configure a Humanitec Sync for Infisical."
+---
+
+**Prerequisites:**
+
+ - Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
+ - Create a [Humanitec Connection](/integrations/app-connections/humanitec)
+
+
+
+ 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
+ 
+
+ 2. Select the **Humanitec** option.
+ 
+
+ 3. Configure the **Source** from where secrets should be retrieved, then click **Next**.
+ 
+
+ - **Environment**: The project environment to retrieve secrets from.
+ - **Secret Path**: The folder path to retrieve secrets from.
+
+
+ If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
+
+
+ 4. Configure the **Destination** to where secrets should be deployed, then click **Next**.
+ 
+
+ - **Humanitec Connection**: The Humanitec Connection to authenticate with.
+ - **Scope**: The Humanitec secret scope to sync secrets to.
+ - **Application**: Sync secrets to a specific application.
+ - **Environment**: Sync secrets to a specific environment of an application.
+
+ The remaining fields are determined by the selected **Scope**:
+
+
+ - **Organization**: The organization to deploy secrets to.
+ - **App**: The application to deploy secrets to.
+
+
+ - **Organization**: The organization to deploy secrets to.
+ - **App**: The application to deploy secrets to.
+ - **Environment**: The environment to deploy secrets to.
+
+
+
+ 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
+ 
+
+ - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
+ - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
+
+ Humanitec does not support importing secrets.
+
+ - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
+ - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
+
+ 6. Configure the **Details** of your Humanitec Sync, then click **Next**.
+ 
+
+ - **Name**: The name of your sync. Must be slug-friendly.
+ - **Description**: An optional description for your sync.
+
+ 7. Review your Humanitec Sync configuration, then click **Create Sync**.
+ 
+
+ 8. If enabled, your Humanitec Sync will begin syncing your secrets to the destination endpoint.
+ 
+
+
+
+ To create an **Humanitec Sync**, make an API request to the [Create Humanitec Sync](/api-reference/endpoints/secret-syncs/humanitec/create) API endpoint.
+
+ ### Sample request
+
+ ```bash Request
+ curl --request POST \
+ --url https://app.infisical.com/api/v1/secret-syncs/humanitec \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "name": "my-humanitec-sync",
+ "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "description": "an example sync",
+ "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "environment": "dev",
+ "secretPath": "/my-secrets",
+ "isEnabled": true,
+ "syncOptions": {
+ "initialSyncBehavior": "overwrite-destination"
+ },
+ "destinationConfig": {
+ "scope": "application",
+ "app": "my-app",
+ "environment": "development"
+ }
+ }'
+ ```
+
+ ### Sample response
+
+ ```bash Response
+ {
+ "secretSync": {
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "name": "my-humanitec-sync",
+ "description": "an example sync",
+ "isEnabled": true,
+ "version": 1,
+ "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "createdAt": "2023-11-07T05:31:56Z",
+ "updatedAt": "2023-11-07T05:31:56Z",
+ "syncStatus": "succeeded",
+ "lastSyncJobId": "123",
+ "lastSyncMessage": null,
+ "lastSyncedAt": "2023-11-07T05:31:56Z",
+ "importStatus": null,
+ "lastImportJobId": null,
+ "lastImportMessage": null,
+ "lastImportedAt": null,
+ "removeStatus": null,
+ "lastRemoveJobId": null,
+ "lastRemoveMessage": null,
+ "lastRemovedAt": null,
+ "syncOptions": {
+ "initialSyncBehavior": "overwrite-destination"
+ },
+ "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "connection": {
+ "app": "humanitec",
+ "name": "my-humanitec-connection",
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
+ },
+ "environment": {
+ "slug": "dev",
+ "name": "Development",
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
+ },
+ "folder": {
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
+ "path": "/my-secrets"
+ },
+ "destination": "humanitec",
+ "destinationConfig": {
+ "scope": "application",
+ "org": "my-organization",
+ "app": "my-app",
+ "env": "development"
+ }
+ }
+ }
+ ```
+
+
diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx
index 6f0a2517a..17daf88cb 100644
--- a/docs/internals/security.mdx
+++ b/docs/internals/security.mdx
@@ -103,7 +103,7 @@ With standby regions and automated failovers in place, Infisical Cloud faces min
Infisical hires external third parties to perform regular security assessment and penetration testing of the platform.
-Most recently, Infisical commissioned cybersecurity firm [Oneleet](https://www.oneleet.com) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.).
+Most recently, Infisical commissioned cybersecurity firm [Cure53](https://cure53.de/) to perform a full-coverage, gray box penetration test against the platform's entire attack surface to identify vulnerabilities according to industry standards (OWASP, ASVS, WSTG, TOP-10, etc.).
Please email security@infisical.com to request any reports including a letter of attestation for the conducted penetration test.
diff --git a/docs/mint.json b/docs/mint.json
index 4ab8f0579..ea454e6a0 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -150,7 +150,14 @@
"pages": [
"documentation/platform/access-controls/overview",
"documentation/platform/access-controls/role-based-access-controls",
- "documentation/platform/access-controls/attribute-based-access-controls",
+ {
+ "group": "Attribute based access controls",
+ "pages": [
+ "documentation/platform/access-controls/abac/overview",
+ "documentation/platform/access-controls/abac/managing-user-metadata",
+ "documentation/platform/access-controls/abac/managing-machine-identity-attributes"
+ ]
+ },
"documentation/platform/access-controls/additional-privileges",
"documentation/platform/access-controls/temporary-access",
"documentation/platform/access-controls/access-requests",
@@ -408,7 +415,8 @@
"integrations/app-connections/azure-key-vault",
"integrations/app-connections/databricks",
"integrations/app-connections/gcp",
- "integrations/app-connections/github"
+ "integrations/app-connections/github",
+ "integrations/app-connections/humanitec"
]
}
]
@@ -426,7 +434,8 @@
"integrations/secret-syncs/azure-key-vault",
"integrations/secret-syncs/databricks",
"integrations/secret-syncs/gcp-secret-manager",
- "integrations/secret-syncs/github"
+ "integrations/secret-syncs/github",
+ "integrations/secret-syncs/humanitec"
]
}
]
@@ -898,6 +907,18 @@
"api-reference/endpoints/app-connections/github/update",
"api-reference/endpoints/app-connections/github/delete"
]
+ },
+ {
+ "group": "Humanitec",
+ "pages": [
+ "api-reference/endpoints/app-connections/humanitec/list",
+ "api-reference/endpoints/app-connections/humanitec/available",
+ "api-reference/endpoints/app-connections/humanitec/get-by-id",
+ "api-reference/endpoints/app-connections/humanitec/get-by-name",
+ "api-reference/endpoints/app-connections/humanitec/create",
+ "api-reference/endpoints/app-connections/humanitec/update",
+ "api-reference/endpoints/app-connections/humanitec/delete"
+ ]
}
]
},
@@ -1001,6 +1022,19 @@
"api-reference/endpoints/secret-syncs/github/sync-secrets",
"api-reference/endpoints/secret-syncs/github/remove-secrets"
]
+ },
+ {
+ "group": "Humanitec",
+ "pages": [
+ "api-reference/endpoints/secret-syncs/humanitec/list",
+ "api-reference/endpoints/secret-syncs/humanitec/get-by-id",
+ "api-reference/endpoints/secret-syncs/humanitec/get-by-name",
+ "api-reference/endpoints/secret-syncs/humanitec/create",
+ "api-reference/endpoints/secret-syncs/humanitec/update",
+ "api-reference/endpoints/secret-syncs/humanitec/delete",
+ "api-reference/endpoints/secret-syncs/humanitec/sync-secrets",
+ "api-reference/endpoints/secret-syncs/humanitec/remove-secrets"
+ ]
}
]
},
diff --git a/frontend/public/images/integrations/Humanitec.png b/frontend/public/images/integrations/Humanitec.png
new file mode 100644
index 000000000..7f763d359
Binary files /dev/null and b/frontend/public/images/integrations/Humanitec.png differ
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx
new file mode 100644
index 000000000..229f54b01
--- /dev/null
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HumanitecSyncFields.tsx
@@ -0,0 +1,197 @@
+import { Controller, useFormContext, useWatch } from "react-hook-form";
+import { SingleValue } from "react-select";
+import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
+import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
+import { HUMANITEC_SYNC_SCOPES } from "@app/helpers/secretSyncs";
+import {
+ THumanitecConnectionApp,
+ THumanitecConnectionEnvironment,
+ THumanitecConnectionOrganization,
+ useHumanitecConnectionListOrganizations
+} from "@app/hooks/api/appConnections/humanitec";
+import { SecretSync } from "@app/hooks/api/secretSyncs";
+import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
+
+import { TSecretSyncForm } from "../schemas";
+
+export const HumanitecSyncFields = () => {
+ const { control, watch, setValue } = useFormContext<
+ TSecretSyncForm & { destination: SecretSync.Humanitec }
+ >();
+
+ const connectionId = useWatch({ name: "connection.id", control });
+ const currentOrg = watch("destinationConfig.org");
+ const currentApp = watch("destinationConfig.app");
+ const currentScope = watch("destinationConfig.scope");
+
+ const { data: organizations = [], isPending: isOrganizationsPending } =
+ useHumanitecConnectionListOrganizations(connectionId, {
+ enabled: Boolean(connectionId)
+ });
+
+ const selectedOrg = organizations?.find((org) => org.id === currentOrg);
+ const selectedApp = selectedOrg?.apps?.find((app) => app.id === currentApp);
+ const environments = selectedApp?.envs || [];
+
+ return (
+ <>
+ {
+ setValue("destinationConfig.org", "");
+ setValue("destinationConfig.app", "");
+ setValue("destinationConfig.env", "");
+ }}
+ />
+ (
+
+ org.id === value) ?? []) : []}
+ onChange={(option) => {
+ onChange((option as SingleValue)?.id ?? null);
+ setValue("destinationConfig.app", "");
+ setValue("destinationConfig.env", "");
+ }}
+ options={organizations}
+ placeholder="Select an organization..."
+ getOptionLabel={(option) => option.name}
+ getOptionValue={(option) => option.id.toString()}
+ />
+
+ )}
+ />
+ (
+
+
+ Don't see the app you're looking for?{" "}
+
+
+
+ }
+ >
+ org.id === currentOrg)
+ ?.apps?.find((app) => app.id === value) ?? null
+ }
+ onChange={(option) => {
+ onChange((option as SingleValue)?.id ?? null);
+ setValue("destinationConfig.env", "");
+ }}
+ options={
+ currentOrg ? (organizations.find((org) => org.id === currentOrg)?.apps ?? []) : []
+ }
+ placeholder="Select an app..."
+ getOptionLabel={(option) => option.name}
+ getOptionValue={(option) => option.id.toString()}
+ />
+
+ )}
+ />
+ (
+
+
+ Specify how Infisical should manage secrets from Humanitec. The following options
+ are available:
+
+
+
+ }
+ >
+
+
+ )}
+ />
+ {currentScope === HumanitecSyncScope.Environment && (
+ (
+
+ env.id === value) ?? null}
+ onChange={(option) =>
+ onChange((option as SingleValue)?.id ?? null)
+ }
+ options={environments}
+ placeholder="Select an env..."
+ getOptionLabel={(option) => option.name}
+ getOptionValue={(option) => option.id.toString()}
+ />
+
+ )}
+ />
+ )}
+ >
+ );
+};
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx
index fa1aaa84c..973f8bf17 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx
@@ -10,6 +10,7 @@ import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
+import { HumanitecSyncFields } from "./HumanitecSyncFields";
export const SecretSyncDestinationFields = () => {
const { watch } = useFormContext();
@@ -31,6 +32,8 @@ export const SecretSyncDestinationFields = () => {
return ;
case SecretSync.Databricks:
return ;
+ case SecretSync.Humanitec:
+ return ;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
index ad9a26408..b14bce809 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
@@ -1,9 +1,9 @@
import { ReactNode } from "react";
import { Controller, useFormContext } from "react-hook-form";
-import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
+import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { FormControl, Select, SelectItem } from "@app/components/v2";
+import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
@@ -38,6 +38,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.AzureKeyVault:
case SecretSync.AzureAppConfiguration:
case SecretSync.Databricks:
+ case SecretSync.Humanitec:
AdditionalSyncOptionsFieldsComponent = null;
break;
default:
@@ -115,6 +116,44 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
>
)}
{AdditionalSyncOptionsFieldsComponent}
+ {
+ return (
+
+
+
+ Disable Secret Deletion{" "}
+
+
+ When enabled, Infisical will not{" "}
+ remove secrets from the destination during a sync.
+
+
+ Enable this option if you intend to manage some secrets manually outside
+ of Infisical.
+
+ >
+ }
+ >
+
+
+
+
+
+ );
+ }}
+ />
{/* (
{
+ const { watch } = useFormContext();
+ const orgId = watch("destinationConfig.org");
+ const appId = watch("destinationConfig.app");
+ const envId = watch("destinationConfig.env");
+ const scope = watch("destinationConfig.scope");
+
+ return (
+ <>
+ {orgId}
+ {appId}
+ {scope === HumanitecSyncScope.Environment && (
+ {envId}
+ )}
+ >
+ );
+};
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
index 2846433ec..040944298 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
@@ -20,6 +20,7 @@ import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
+import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext();
@@ -35,6 +36,7 @@ export const SecretSyncReviewFields = () => {
secretPath,
syncOptions: {
// appendSuffix, prependPrefix,
+ disableSecretDeletion,
initialSyncBehavior
},
destination,
@@ -67,6 +69,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Databricks:
DestinationFieldsComponent = ;
break;
+ case SecretSync.Humanitec:
+ DestinationFieldsComponent = ;
+ break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -107,6 +112,11 @@ export const SecretSyncReviewFields = () => {
{/* {prependPrefix}
{appendSuffix} */}
{AdditionalSyncOptionsFieldsComponent}
+ {disableSecretDeletion && (
+
+ Disabled
+
+ )}
diff --git a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts
index b898fe91f..bf72321ce 100644
--- a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts
+++ b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts
@@ -7,7 +7,8 @@ export const BaseSecretSyncSchema =
{
const baseSyncOptionsSchema = z.object({
- initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior)
+ initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior),
+ disableSecretDeletion: z.boolean().optional().default(false)
// scott: removed temporarily for evaluation of template formatting
// prependPrefix: z
// .string()
diff --git a/frontend/src/components/secret-syncs/forms/schemas/humanitec-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/humanitec-sync-destination-schema.ts
new file mode 100644
index 000000000..bb438557f
--- /dev/null
+++ b/frontend/src/components/secret-syncs/forms/schemas/humanitec-sync-destination-schema.ts
@@ -0,0 +1,24 @@
+import { z } from "zod";
+
+import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
+import { SecretSync } from "@app/hooks/api/secretSyncs";
+import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
+
+export const HumanitecSyncDestinationSchema = BaseSecretSyncSchema().merge(
+ z.object({
+ destination: z.literal(SecretSync.Humanitec),
+ destinationConfig: z.discriminatedUnion("scope", [
+ z.object({
+ scope: z.literal(HumanitecSyncScope.Application),
+ org: z.string().trim().min(1, "Organization required"),
+ app: z.string().trim().min(1, "Application required")
+ }),
+ z.object({
+ scope: z.literal(HumanitecSyncScope.Environment),
+ org: z.string().trim().min(1, "Organization required"),
+ app: z.string().trim().min(1, "Application required"),
+ env: z.string().trim().min(1, "Environment required")
+ })
+ ])
+ })
+);
diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts
index 55ee6cb3d..be2322304 100644
--- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts
+++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts
@@ -8,6 +8,7 @@ import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sy
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
+import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
@@ -16,7 +17,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
GcpSyncDestinationSchema,
AzureKeyVaultSyncDestinationSchema,
AzureAppConfigurationSyncDestinationSchema,
- DatabricksSyncDestinationSchema
+ DatabricksSyncDestinationSchema,
+ HumanitecSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;
diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts
index c36264f5c..2be3f3dbc 100644
--- a/frontend/src/const/routes.ts
+++ b/frontend/src/const/routes.ts
@@ -23,7 +23,11 @@ export const ROUTE_PATHS = Object.freeze({
),
SecretSharing: setRoute(
"/organization/secret-sharing",
- "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
+ ),
+ SecretSharingSettings: setRoute(
+ "/organization/secret-sharing/settings",
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
),
SettingsPage: setRoute(
"/organization/settings",
diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts
index c2873e6f9..2c742db14 100644
--- a/frontend/src/context/OrgPermissionContext/types.ts
+++ b/frontend/src/context/OrgPermissionContext/types.ts
@@ -34,13 +34,18 @@ export enum OrgPermissionSubjects {
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip",
- Gateway = "gateway"
+ Gateway = "gateway",
+ SecretShare = "secret-share"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
+export enum OrgPermissionSecretShareAction {
+ ManageSettings = "manage-settings"
+}
+
export enum OrgPermissionAppConnectionActions {
Read = "read",
Create = "create",
@@ -100,8 +105,8 @@ export type OrgPermissionSet =
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
- | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway];
-
+ | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
+ | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
// TODO(scott): add back once org UI refactored
// | [
// OrgPermissionAppConnectionActions,
diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts
index 016a86264..cf643218b 100644
--- a/frontend/src/helpers/appConnections.ts
+++ b/frontend/src/helpers/appConnections.ts
@@ -11,6 +11,7 @@ import {
TAppConnection
} from "@app/hooks/api/appConnections/types";
import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection";
+import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection";
export const APP_CONNECTION_MAP: Record = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
@@ -24,7 +25,8 @@ export const APP_CONNECTION_MAP: Record {
@@ -43,6 +45,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Service Account Impersonation", icon: faUser };
case DatabricksConnectionMethod.ServicePrincipal:
return { name: "Service Principal", icon: faUser };
+ case HumanitecConnectionMethod.API_TOKEN:
+ return { name: "API Token", icon: faKey };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}
diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts
index b119983a0..1cc076cce 100644
--- a/frontend/src/helpers/secretSyncs.ts
+++ b/frontend/src/helpers/secretSyncs.ts
@@ -4,6 +4,7 @@ import {
SecretSyncImportBehavior,
SecretSyncInitialSyncBehavior
} from "@app/hooks/api/secretSyncs";
+import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
export const SECRET_SYNC_MAP: Record = {
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
@@ -18,6 +19,10 @@ export const SECRET_SYNC_MAP: Record = {
[SecretSync.GCPSecretManager]: AppConnection.GCP,
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
- [SecretSync.Databricks]: AppConnection.Databricks
+ [SecretSync.Databricks]: AppConnection.Databricks,
+ [SecretSync.Humanitec]: AppConnection.Humanitec
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
@@ -62,3 +68,19 @@ export const SECRET_SYNC_IMPORT_BEHAVIOR_MAP: Record<
description: `Infisical will import any secrets present in the ${destinationName} destination, prioritizing values from ${destinationName} over Infisical when keys conflict.`
})
};
+
+export const HUMANITEC_SYNC_SCOPES: Record<
+ HumanitecSyncScope,
+ { name: string; description: string }
+> = {
+ [HumanitecSyncScope.Application]: {
+ name: "Application",
+ description:
+ "Infisical will sync secrets as application level shared values to the specified Humanitec application."
+ },
+ [HumanitecSyncScope.Environment]: {
+ name: "Environment",
+ description:
+ "Infisical will sync secrets as environment level shared values to the specified Humanitec application environment."
+ }
+};
diff --git a/frontend/src/hooks/api/admin/queries.ts b/frontend/src/hooks/api/admin/queries.ts
index 496990abe..b24841dbd 100644
--- a/frontend/src/hooks/api/admin/queries.ts
+++ b/frontend/src/hooks/api/admin/queries.ts
@@ -1,6 +1,7 @@
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
+import { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types";
import {
@@ -10,7 +11,6 @@ import {
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
-import { Identity } from "@app/hooks/api/identities/types";
export const adminStandaloneKeys = {
getUsers: "get-users",
diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts
index 97fcf9be6..c07808af9 100644
--- a/frontend/src/hooks/api/appConnections/enums.ts
+++ b/frontend/src/hooks/api/appConnections/enums.ts
@@ -4,5 +4,6 @@ export enum AppConnection {
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
- Databricks = "databricks"
+ Databricks = "databricks",
+ Humanitec = "humanitec"
}
diff --git a/frontend/src/hooks/api/appConnections/humanitec/index.ts b/frontend/src/hooks/api/appConnections/humanitec/index.ts
new file mode 100644
index 000000000..2c1906d36
--- /dev/null
+++ b/frontend/src/hooks/api/appConnections/humanitec/index.ts
@@ -0,0 +1,2 @@
+export * from "./queries";
+export * from "./types";
diff --git a/frontend/src/hooks/api/appConnections/humanitec/queries.tsx b/frontend/src/hooks/api/appConnections/humanitec/queries.tsx
new file mode 100644
index 000000000..d53913e37
--- /dev/null
+++ b/frontend/src/hooks/api/appConnections/humanitec/queries.tsx
@@ -0,0 +1,37 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { appConnectionKeys } from "../queries";
+import { THumanitecOrganization } from "./types";
+
+const humanitecConnectionKeys = {
+ all: [...appConnectionKeys.all, "humanitec"] as const,
+ listOrganizations: (connectionId: string) =>
+ [...humanitecConnectionKeys.all, "organizations", connectionId] as const
+};
+
+export const useHumanitecConnectionListOrganizations = (
+ connectionId: string,
+ options?: Omit<
+ UseQueryOptions<
+ THumanitecOrganization[],
+ unknown,
+ THumanitecOrganization[],
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >
+) => {
+ return useQuery({
+ queryKey: humanitecConnectionKeys.listOrganizations(connectionId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get(
+ `/api/v1/app-connections/humanitec/${connectionId}/organizations`
+ );
+
+ return data;
+ },
+ ...options
+ });
+};
diff --git a/frontend/src/hooks/api/appConnections/humanitec/types.ts b/frontend/src/hooks/api/appConnections/humanitec/types.ts
new file mode 100644
index 000000000..5ab1b4d31
--- /dev/null
+++ b/frontend/src/hooks/api/appConnections/humanitec/types.ts
@@ -0,0 +1,28 @@
+export type THumanitecOrganization = {
+ name: string;
+ id: string;
+ apps: THumanitecApp[];
+};
+
+export type THumanitecApp = {
+ id: string;
+ name: string;
+ envs: { id: string; name: string }[];
+};
+
+export type THumanitecConnectionApp = {
+ id: string;
+ name: string;
+ envs: THumanitecConnectionEnvironment[];
+};
+
+export type THumanitecConnectionEnvironment = {
+ id: string;
+ name: string;
+};
+
+export type THumanitecConnectionOrganization = {
+ id: string;
+ name: string;
+ apps: THumanitecConnectionApp[];
+};
diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts
index e42593c94..a323765f7 100644
--- a/frontend/src/hooks/api/appConnections/types/app-options.ts
+++ b/frontend/src/hooks/api/appConnections/types/app-options.ts
@@ -34,13 +34,18 @@ export type TDatabricksConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Databricks;
};
+export type THumanitecConnectionOption = TAppConnectionOptionBase & {
+ app: AppConnection.Humanitec;
+};
+
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
| TGcpConnectionOption
| TAzureAppConfigurationConnectionOption
| TAzureKeyVaultConnectionOption
- | TDatabricksConnectionOption;
+ | TDatabricksConnectionOption
+ | THumanitecConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -49,4 +54,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnectionOption;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
[AppConnection.Databricks]: TDatabricksConnectionOption;
+ [AppConnection.Humanitec]: THumanitecConnectionOption;
};
diff --git a/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts b/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts
new file mode 100644
index 000000000..2473050cd
--- /dev/null
+++ b/frontend/src/hooks/api/appConnections/types/humanitec-connection.ts
@@ -0,0 +1,13 @@
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
+
+export enum HumanitecConnectionMethod {
+ API_TOKEN = "api-token"
+}
+
+export type THumanitecConnection = TRootAppConnection & { app: AppConnection.Humanitec } & {
+ method: HumanitecConnectionMethod.API_TOKEN;
+ credentials: {
+ apiToken: string;
+ };
+};
diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts
index a2a1b6792..db56839fc 100644
--- a/frontend/src/hooks/api/appConnections/types/index.ts
+++ b/frontend/src/hooks/api/appConnections/types/index.ts
@@ -3,6 +3,7 @@ import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-op
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
import { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection";
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
+import { THumanitecConnection } from "@app/hooks/api/appConnections/types/humanitec-connection";
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
@@ -13,6 +14,7 @@ export * from "./azure-app-configuration-connection";
export * from "./azure-key-vault-connection";
export * from "./gcp-connection";
export * from "./github-connection";
+export * from "./humanitec-connection";
export type TAppConnection =
| TAwsConnection
@@ -20,7 +22,8 @@ export type TAppConnection =
| TGcpConnection
| TAzureKeyVaultConnection
| TAzureAppConfigurationConnection
- | TDatabricksConnection;
+ | TDatabricksConnection
+ | THumanitecConnection;
export type TAvailableAppConnection = Pick;
@@ -54,4 +57,5 @@ export type TAppConnectionMap = {
[AppConnection.AzureKeyVault]: TAzureKeyVaultConnection;
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.Databricks]: TDatabricksConnection;
+ [AppConnection.Humanitec]: THumanitecConnection;
};
diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx
index 3e38d9067..d68595ad5 100644
--- a/frontend/src/hooks/api/identities/mutations.tsx
+++ b/frontend/src/hooks/api/identities/mutations.tsx
@@ -462,6 +462,7 @@ export const useUpdateIdentityOidcAuth = () => {
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject
}) => {
const {
@@ -478,7 +479,8 @@ export const useUpdateIdentityOidcAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
- accessTokenTrustedIps
+ accessTokenTrustedIps,
+ claimMetadataMapping
}
);
@@ -504,6 +506,7 @@ export const useAddIdentityOidcAuth = () => {
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject,
accessTokenTTL,
accessTokenMaxTTL,
@@ -524,7 +527,8 @@ export const useAddIdentityOidcAuth = () => {
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
- accessTokenTrustedIps
+ accessTokenTrustedIps,
+ claimMetadataMapping
}
);
diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts
index 8d344e7f7..86d35e086 100644
--- a/frontend/src/hooks/api/identities/types.ts
+++ b/frontend/src/hooks/api/identities/types.ts
@@ -193,6 +193,7 @@ export type IdentityOidcAuth = {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record;
+ claimMetadataMapping?: Record;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@@ -208,6 +209,7 @@ export type AddIdentityOidcAuthDTO = {
boundIssuer: string;
boundAudiences: string;
boundClaims: Record;
+ claimMetadataMapping?: Record;
boundSubject: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
@@ -225,6 +227,7 @@ export type UpdateIdentityOidcAuthDTO = {
boundIssuer?: string;
boundAudiences?: string;
boundClaims?: Record;
+ claimMetadataMapping?: Record;
boundSubject?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx
index ca36179d0..90e525cdf 100644
--- a/frontend/src/hooks/api/organization/queries.tsx
+++ b/frontend/src/hooks/api/organization/queries.tsx
@@ -109,7 +109,8 @@ export const useUpdateOrg = () => {
orgId,
defaultMembershipRoleSlug,
enforceMfa,
- selectedMfaMethod
+ selectedMfaMethod,
+ allowSecretSharingOutsideOrganization
}) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
@@ -118,7 +119,8 @@ export const useUpdateOrg = () => {
slug,
defaultMembershipRoleSlug,
enforceMfa,
- selectedMfaMethod
+ selectedMfaMethod,
+ allowSecretSharingOutsideOrganization
});
},
onSuccess: () => {
diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts
index 219dd7c10..c563d8c43 100644
--- a/frontend/src/hooks/api/organization/types.ts
+++ b/frontend/src/hooks/api/organization/types.ts
@@ -16,6 +16,7 @@ export type Organization = {
enforceMfa: boolean;
selectedMfaMethod?: MfaMethod;
shouldUseNewPrivilegeSystem: boolean;
+ allowSecretSharingOutsideOrganization?: boolean;
};
export type UpdateOrgDTO = {
@@ -27,6 +28,7 @@ export type UpdateOrgDTO = {
defaultMembershipRoleSlug?: string;
enforceMfa?: boolean;
selectedMfaMethod?: MfaMethod;
+ allowSecretSharingOutsideOrganization?: boolean;
};
export type BillingDetails = {
diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts
index c942c388d..08accba16 100644
--- a/frontend/src/hooks/api/secretSyncs/enums.ts
+++ b/frontend/src/hooks/api/secretSyncs/enums.ts
@@ -5,7 +5,8 @@ export enum SecretSync {
GCPSecretManager = "gcp-secret-manager",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
- Databricks = "databricks"
+ Databricks = "databricks",
+ Humanitec = "humanitec"
}
export enum SecretSyncStatus {
diff --git a/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts b/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts
new file mode 100644
index 000000000..5dc06e031
--- /dev/null
+++ b/frontend/src/hooks/api/secretSyncs/types/humanitec-sync.ts
@@ -0,0 +1,29 @@
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+import { SecretSync } from "@app/hooks/api/secretSyncs";
+import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
+
+export type THumanitecSync = TRootSecretSync & {
+ destination: SecretSync.Humanitec;
+ destinationConfig:
+ | {
+ scope: HumanitecSyncScope.Application;
+ org: string;
+ app: string;
+ }
+ | {
+ scope: HumanitecSyncScope.Environment;
+ org: string;
+ app: string;
+ env: string;
+ };
+ connection: {
+ app: AppConnection.Humanitec;
+ name: string;
+ id: string;
+ };
+};
+
+export enum HumanitecSyncScope {
+ Application = "application",
+ Environment = "environment"
+}
diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts
index e797c6458..a90a8a3ef 100644
--- a/frontend/src/hooks/api/secretSyncs/types/index.ts
+++ b/frontend/src/hooks/api/secretSyncs/types/index.ts
@@ -8,6 +8,7 @@ import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
import { TGcpSync } from "./gcp-sync";
+import { THumanitecSync } from "./humanitec-sync";
export type TSecretSyncOption = {
name: string;
@@ -22,7 +23,8 @@ export type TSecretSync =
| TGcpSync
| TAzureKeyVaultSync
| TAzureAppConfigurationSync
- | TDatabricksSync;
+ | TDatabricksSync
+ | THumanitecSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
diff --git a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts
index 49cf9c978..dfbbcd063 100644
--- a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts
+++ b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts
@@ -3,6 +3,7 @@ import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/
export type RootSyncOptions = {
initialSyncBehavior: SecretSyncInitialSyncBehavior;
+ disableSecretDeletion?: boolean;
// prependPrefix?: string;
// appendSuffix?: string;
};
diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx
index 0e71c7bb6..7cfc21838 100644
--- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx
+++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx
@@ -1,30 +1,33 @@
import { useTranslation } from "react-i18next";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Link, linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
+import { linkOptions, Outlet, useLocation, useRouterState } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Banner } from "@app/components/page-frames/Banner";
-import {
- BreadcrumbContainer,
- Menu,
- MenuGroup,
- MenuItem,
- TBreadcrumbFormat
-} from "@app/components/v2";
-import { useServerConfig } from "@app/context";
+import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
+import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
+import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { SidebarHeader } from "./components/SidebarHeader";
+import { DefaultSideBar, SecretSharingSideBar } from "./ProductsSideBar";
export const OrganizationLayout = () => {
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const location = useLocation();
const { config } = useServerConfig();
+ const { permission } = useOrgPermission();
+
+ const shouldShowProductsSidebar = permission.can(
+ OrgPermissionSecretShareAction.ManageSettings,
+ OrgPermissionSubjects.SecretShare
+ );
+
const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
const breadcrumbs =
isOrganizationSpecificPage && matches && "breadcrumbs" in matches
@@ -35,16 +38,23 @@ export const OrganizationLayout = () => {
const { t } = useTranslation();
+ const isSecretSharingPage = (
+ [
+ linkOptions({ to: "/organization/secret-sharing" }).to,
+ linkOptions({ to: "/organization/secret-sharing/settings" }).to
+ ] as string[]
+ ).includes(location.pathname);
+
const shouldShowOrgSidebar =
location.pathname.startsWith("/organization") &&
+ (!isSecretSharingPage || shouldShowProductsSidebar) &&
!(
[
linkOptions({ to: "/organization/secret-manager/overview" }).to,
linkOptions({ to: "/organization/cert-manager/overview" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/kms/overview" }).to,
- linkOptions({ to: "/organization/secret-scanning" }).to,
- linkOptions({ to: "/organization/secret-sharing" }).to
+ linkOptions({ to: "/organization/secret-scanning" }).to
] as string[]
).includes(location.pathname);
@@ -73,58 +83,7 @@ export const OrganizationLayout = () => {
-
+ {isSecretSharingPage ? : }
)}
diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx
new file mode 100644
index 000000000..e78d86703
--- /dev/null
+++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx
@@ -0,0 +1,58 @@
+import { Link } from "@tanstack/react-router";
+
+import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
+
+export const DefaultSideBar = () => (
+
+);
diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/SecretSharingSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/SecretSharingSideBar.tsx
new file mode 100644
index 000000000..fbfc018c0
--- /dev/null
+++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/SecretSharingSideBar.tsx
@@ -0,0 +1,34 @@
+import { Link, useMatchRoute } from "@tanstack/react-router";
+
+import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
+
+export const SecretSharingSideBar = () => {
+ const matchRoute = useMatchRoute();
+ const isOverviewActive = !!matchRoute({
+ to: "/organization/secret-sharing",
+ fuzzy: false
+ });
+
+ return (
+
+ );
+};
diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts
new file mode 100644
index 000000000..b2c79d873
--- /dev/null
+++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/index.ts
@@ -0,0 +1,2 @@
+export * from "./DefaultSideBar";
+export * from "./SecretSharingSideBar";
diff --git a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
index 776c5e20f..8d1e25bc5 100644
--- a/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/admin/OverviewPage/OverviewPage.tsx
@@ -28,13 +28,13 @@ import {
useGetServerRootKmsEncryptionDetails,
useUpdateServerConfig
} from "@app/hooks/api";
+import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
import { AuthPanel } from "./components/AuthPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { RateLimitPanel } from "./components/RateLimitPanel";
import { UserPanel } from "./components/UserPanel";
-import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
enum TabSections {
Settings = "settings",
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx
index b1b0739cf..c0a91a7f7 100644
--- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx
+++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx
@@ -46,12 +46,22 @@ const schema = z.object({
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: z.string().optional().default(""),
- boundClaims: z.array(
- z.object({
- key: z.string(),
- value: z.string()
- })
- ),
+ boundClaims: z
+ .array(
+ z.object({
+ key: z.string(),
+ value: z.string()
+ })
+ )
+ .default([]),
+ claimMetadataMapping: z
+ .array(
+ z.object({
+ key: z.string(),
+ value: z.string()
+ })
+ )
+ .default([]),
boundSubject: z.string().optional().default("")
});
@@ -96,10 +106,11 @@ export const IdentityOidcAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
- accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
+ boundClaims: [],
+ claimMetadataMapping: []
}
});
-
const {
fields: boundClaimsFields,
append: appendBoundClaimField,
@@ -109,6 +120,15 @@ export const IdentityOidcAuthForm = ({
name: "boundClaims"
});
+ const {
+ fields: claimMetadataMappingFields,
+ append: appendClaimMetadataMappingField,
+ remove: removeClaimMetadataMappingField
+ } = useFieldArray({
+ control,
+ name: "claimMetadataMapping"
+ });
+
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
@@ -126,6 +146,12 @@ export const IdentityOidcAuthForm = ({
key,
value
})),
+ claimMetadataMapping: data?.claimMetadataMapping
+ ? Object.entries(data.claimMetadataMapping).map(([key, value]) => ({
+ key,
+ value
+ }))
+ : undefined,
boundSubject: data.boundSubject,
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
@@ -149,7 +175,8 @@ export const IdentityOidcAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
- accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
+ accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
+ claimMetadataMapping: []
});
}
}, [data]);
@@ -164,6 +191,7 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims,
+ claimMetadataMapping,
boundSubject
}: FormData) => {
try {
@@ -180,6 +208,9 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
+ claimMetadataMapping: claimMetadataMapping
+ ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
+ : undefined,
boundSubject,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
@@ -194,6 +225,9 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
+ claimMetadataMapping: claimMetadataMapping
+ ? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
+ : undefined,
boundSubject,
organizationId: orgId,
accessTokenTTL: Number(accessTokenTTL),
@@ -223,7 +257,9 @@ export const IdentityOidcAuthForm = ({
+ ))}
+
+
+
+
{accessTokenTrustedIpsFields.map(({ id }, index) => (
-
-
-
+
+
+
+
+
);
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
index c84b20885..fc3257841 100644
--- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx
@@ -15,6 +15,7 @@ import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
+import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
@@ -62,6 +63,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return
;
case AppConnection.Databricks:
return
;
+ case AppConnection.Humanitec:
+ return
;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -107,6 +110,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return
;
case AppConnection.Databricks:
return
;
+ case AppConnection.Humanitec:
+ return
;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}
diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx
new file mode 100644
index 000000000..b119f46ad
--- /dev/null
+++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HumanitecConnectionForm.tsx
@@ -0,0 +1,132 @@
+import { Controller, FormProvider, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import {
+ Button,
+ FormControl,
+ ModalClose,
+ SecretInput,
+ Select,
+ SelectItem
+} from "@app/components/v2";
+import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
+import { HumanitecConnectionMethod, THumanitecConnection } from "@app/hooks/api/appConnections";
+import { AppConnection } from "@app/hooks/api/appConnections/enums";
+
+import {
+ genericAppConnectionFieldsSchema,
+ GenericAppConnectionsFields
+} from "./GenericAppConnectionFields";
+
+type Props = {
+ appConnection?: THumanitecConnection;
+ onSubmit: (formData: FormData) => void;
+};
+
+const rootSchema = genericAppConnectionFieldsSchema.extend({
+ app: z.literal(AppConnection.Humanitec)
+});
+
+const formSchema = z.discriminatedUnion("method", [
+ rootSchema.extend({
+ method: z.literal(HumanitecConnectionMethod.API_TOKEN),
+ credentials: z.object({
+ apiToken: z.string().trim().min(1, "Service API Token required")
+ })
+ })
+]);
+
+type FormData = z.infer
;
+
+export const HumanitecConnectionForm = ({ appConnection, onSubmit }: Props) => {
+ const isUpdate = Boolean(appConnection);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: appConnection ?? {
+ app: AppConnection.Humanitec,
+ method: HumanitecConnectionMethod.API_TOKEN
+ }
+ });
+
+ const {
+ handleSubmit,
+ control,
+ formState: { isSubmitting, isDirty }
+ } = form;
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx
index cdf5e9325..116ecfd38 100644
--- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx
+++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx
@@ -111,6 +111,26 @@ export const ViewIdentityOidcAuthContent = ({
)}
+
+ {data.claimMetadataMapping && Object.keys(data.claimMetadataMapping).length && (
+
+ {JSON.stringify(data.claimMetadataMapping, null, 2)}
+
+ }
+ >
+
+
+
+ Reveal
+
+
+
+ )}
+
);
};
diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts
index 56d77ae60..09791dec1 100644
--- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts
+++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts
@@ -7,7 +7,8 @@ import {
OrgPermissionAppConnectionActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
- OrgPermissionKmipActions
+ OrgPermissionKmipActions,
+ OrgPermissionSecretShareAction
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
@@ -78,6 +79,12 @@ const adminConsolePermissionSchmea = z
})
.optional();
+const secretSharingPermissionSchema = z
+ .object({
+ [OrgPermissionSecretShareAction.ManageSettings]: z.boolean().optional()
+ })
+ .optional();
+
export const formSchema = z.object({
name: z.string().trim(),
description: z.string().trim().optional(),
@@ -111,7 +118,8 @@ export const formSchema = z.object({
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema,
- gateway: orgGatewayPermissionSchema
+ gateway: orgGatewayPermissionSchema,
+ "secret-share": secretSharingPermissionSchema
})
.optional()
});
diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSecretShareRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSecretShareRow.tsx
new file mode 100644
index 000000000..4a35644c7
--- /dev/null
+++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSecretShareRow.tsx
@@ -0,0 +1,130 @@
+import { useEffect, useMemo } from "react";
+import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
+import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { createNotification } from "@app/components/notifications";
+import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
+import { useToggle } from "@app/hooks";
+
+import { TFormSchema } from "../OrgRoleModifySection.utils";
+
+type Props = {
+ isEditable: boolean;
+ setValue: UseFormSetValue;
+ control: Control;
+};
+
+enum Permission {
+ NoAccess = "no-access",
+ Custom = "custom"
+}
+
+const PERMISSION_ACTIONS = [{ action: "manage-settings", label: "Manage settings" }] as const;
+
+export const OrgPermissionSecretShareRow = ({ isEditable, control, setValue }: Props) => {
+ const [isRowExpanded, setIsRowExpanded] = useToggle();
+ const [isCustom, setIsCustom] = useToggle();
+
+ const rule = useWatch({
+ control,
+ name: "permissions.secret-share"
+ });
+
+ const selectedPermissionCategory = useMemo(() => {
+ if (rule?.["manage-settings"]) {
+ return Permission.Custom;
+ }
+ return Permission.NoAccess;
+ }, [rule, isCustom]);
+
+ useEffect(() => {
+ if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
+ else setIsCustom.off();
+ }, [selectedPermissionCategory]);
+
+ useEffect(() => {
+ const isRowCustom = selectedPermissionCategory === Permission.Custom;
+ if (isRowCustom) {
+ setIsRowExpanded.on();
+ }
+ }, []);
+
+ const handlePermissionChange = (val: Permission) => {
+ if (!val) return;
+ if (val === Permission.Custom) {
+ setIsRowExpanded.on();
+ setIsCustom.on();
+ return;
+ }
+ setIsCustom.off();
+
+ if (val === Permission.NoAccess) {
+ setValue("permissions.secret-share", { "manage-settings": false }, { shouldDirty: true });
+ }
+ };
+
+ return (
+ <>
+ setIsRowExpanded.toggle()}
+ >
+ |
+
+ |
+ Secret Share |
+
+
+ |
+
+ {isRowExpanded && (
+
+ |
+
+ {PERMISSION_ACTIONS.map(({ action, label }) => {
+ return (
+ (
+ {
+ if (!isEditable) {
+ createNotification({
+ type: "error",
+ text: "Failed to update default role"
+ });
+ return;
+ }
+ field.onChange(e);
+ }}
+ id={`permissions.secret-share.${action}`}
+ >
+ {label}
+
+ )}
+ />
+ );
+ })}
+
+ |
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx
index aed309d04..0bc765eb0 100644
--- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx
+++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx
@@ -74,7 +74,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude,
- "workspace" | "organization-admin-console" | "kmip" | "gateway"
+ "workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share"
>;
setValue: UseFormSetValue;
control: Control;
diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx
index 1816f4292..f077a63a7 100644
--- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx
+++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx
@@ -18,6 +18,7 @@ import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
+import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@@ -181,6 +182,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
+
{
+ const { currentOrg } = useOrganization();
return (
{
diff --git a/frontend/src/pages/organization/SecretSharingPage/route.tsx b/frontend/src/pages/organization/SecretSharingPage/route.tsx
index 827fc2623..2040fd1bb 100644
--- a/frontend/src/pages/organization/SecretSharingPage/route.tsx
+++ b/frontend/src/pages/organization/SecretSharingPage/route.tsx
@@ -11,7 +11,7 @@ const SecretSharingQueryParams = z.object({
});
export const Route = createFileRoute(
- "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"
)({
component: SecretSharingPage,
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx
new file mode 100644
index 000000000..8ee37f631
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx
@@ -0,0 +1,35 @@
+import { Helmet } from "react-helmet";
+import { useTranslation } from "react-i18next";
+
+import { PageHeader } from "@app/components/v2";
+import {
+ OrgPermissionSecretShareAction,
+ OrgPermissionSubjects
+} from "@app/context/OrgPermissionContext/types";
+import { withPermission } from "@app/hoc";
+
+import { SecretSharingSettingsTabGroup } from "./components";
+
+export const SecretSharingSettingsPage = withPermission(
+ () => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+ {t("common.head-title", { title: t("settings.org.title") })}
+
+
+ >
+ );
+ },
+ {
+ action: OrgPermissionSecretShareAction.ManageSettings,
+ subject: OrgPermissionSubjects.SecretShare
+ }
+);
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/SecretSharingAllowShareToAnyone.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/SecretSharingAllowShareToAnyone.tsx
new file mode 100644
index 000000000..1ea62c187
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/SecretSharingAllowShareToAnyone.tsx
@@ -0,0 +1,58 @@
+import { createNotification } from "@app/components/notifications";
+import { OrgPermissionCan } from "@app/components/permissions";
+import { Switch } from "@app/components/v2";
+import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
+import { useUpdateOrg } from "@app/hooks/api";
+
+export const SecretSharingAllowShareToAnyone = () => {
+ const { currentOrg } = useOrganization();
+ const { mutateAsync } = useUpdateOrg();
+
+ const handleSecretSharingToggle = async (value: boolean) => {
+ try {
+ if (!currentOrg?.id) return;
+
+ await mutateAsync({
+ orgId: currentOrg.id,
+ allowSecretSharingOutsideOrganization: value
+ });
+
+ createNotification({
+ text: `Successfully ${value ? "enabled" : "disabled"} secret sharing to members outside of this organization`,
+ type: "success"
+ });
+ } catch (err) {
+ console.error(err);
+ createNotification({
+ text: (err as { response: { data: { message: string } } }).response.data.message,
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+
+
+
+ Allow sharing secrets to members outside of this organization
+
+
+ {(isAllowed) => (
+ handleSecretSharingToggle(value)}
+ isChecked={currentOrg?.allowSecretSharingOutsideOrganization ?? false}
+ isDisabled={!isAllowed}
+ />
+ )}
+
+
+
+ If enabled, team members will be able to share secrets to members outside of this
+ organization
+
+
+
+ );
+};
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx
new file mode 100644
index 000000000..d02460498
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingAllowShareToAnyone/index.tsx
@@ -0,0 +1 @@
+export { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx
new file mode 100644
index 000000000..ede3d9fc8
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx
@@ -0,0 +1,9 @@
+import { SecretSharingAllowShareToAnyone } from "../SecretSharingAllowShareToAnyone";
+
+export const SecretSharingSettingsGeneralTab = () => {
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx
new file mode 100644
index 000000000..306109025
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/index.tsx
@@ -0,0 +1 @@
+export { SecretSharingSettingsGeneralTab } from "./SecretSharingSettingsGeneralTab";
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx
new file mode 100644
index 000000000..594df474f
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/SecretSharingSettingsTabGroup.tsx
@@ -0,0 +1,39 @@
+import { useState } from "react";
+import { useSearch } from "@tanstack/react-router";
+
+import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
+import { ROUTE_PATHS } from "@app/const/routes";
+
+import { SecretSharingSettingsGeneralTab } from "../SecretSharingSettingsGeneralTab";
+
+export const SecretSharingSettingsTabGroup = () => {
+ const search = useSearch({
+ from: ROUTE_PATHS.Organization.SecretSharingSettings.id
+ });
+ const tabs = [
+ {
+ name: "General",
+ key: "tab-secret-sharing-general",
+ component: SecretSharingSettingsGeneralTab
+ }
+ ];
+
+ const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
+
+ return (
+
+
+ {tabs.map((tab) => (
+
+ {tab.name}
+
+ ))}
+
+ {tabs.map(({ key, component: Component }) => (
+
+
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx
new file mode 100644
index 000000000..2c60c7e20
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsTabGroup/index.tsx
@@ -0,0 +1 @@
+export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx
new file mode 100644
index 000000000..2c60c7e20
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/index.tsx
@@ -0,0 +1 @@
+export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx
new file mode 100644
index 000000000..b7a9e128d
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/route.tsx
@@ -0,0 +1,37 @@
+import { faHome } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
+import { zodValidator } from "@tanstack/zod-adapter";
+import { z } from "zod";
+
+import { SecretSharingSettingsPage } from "./SecretSharingSettingsPage";
+
+const SettingsPageQueryParams = z.object({
+ selectedTab: z.string().catch("")
+});
+
+export const Route = createFileRoute(
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
+)({
+ component: SecretSharingSettingsPage,
+ validateSearch: zodValidator(SettingsPageQueryParams),
+ search: {
+ middlewares: [stripSearchParams({ selectedTab: "" })]
+ },
+ context: () => ({
+ breadcrumbs: [
+ {
+ label: "Home",
+ icon: () => ,
+ link: linkOptions({ to: "/" })
+ },
+ {
+ label: "Secret Sharing",
+ link: linkOptions({ to: "/organization/secret-sharing" })
+ },
+ {
+ label: "Settings"
+ }
+ ]
+ })
+});
diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
index 8e3d34b5a..a2228e46b 100644
--- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
+++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
@@ -41,9 +41,14 @@ export type FormData = z.infer;
type Props = {
isPublic: boolean; // whether or not this is a public (non-authenticated) secret sharing form
value?: string;
+ allowSecretSharingOutsideOrganization?: boolean;
};
-export const ShareSecretForm = ({ isPublic, value }: Props) => {
+export const ShareSecretForm = ({
+ isPublic,
+ value,
+ allowSecretSharingOutsideOrganization = true
+}: Props) => {
const [secretLink, setSecretLink] = useState("");
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset({
initialState: "Copy to clipboard"
@@ -230,7 +235,9 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
onValueChange={(e) => onChange(e)}
className="w-full"
>
- Anyone
+ {allowSecretSharingOutsideOrganization && (
+ Anyone
+ )}
People within your organization
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HumanitecSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HumanitecSyncDestinationCol.tsx
new file mode 100644
index 000000000..b0fffebfe
--- /dev/null
+++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HumanitecSyncDestinationCol.tsx
@@ -0,0 +1,14 @@
+import { THumanitecSync } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
+
+import { getSecretSyncDestinationColValues } from "../helpers";
+import { SecretSyncTableCell } from "../SecretSyncTableCell";
+
+type Props = {
+ secretSync: THumanitecSync;
+};
+
+export const HumanitecSyncDestinationCol = ({ secretSync }: Props) => {
+ const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
+
+ return ;
+};
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx
index 2c375a8b5..c4dc564b0 100644
--- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx
+++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx
@@ -7,6 +7,7 @@ import { AzureKeyVaultDestinationSyncCol } from "./AzureKeyVaultDestinationSyncC
import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
+import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
type Props = {
secretSync: TSecretSync;
@@ -28,6 +29,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return ;
case SecretSync.Databricks:
return ;
+ case SecretSync.Humanitec:
+ return ;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts
index f1087c03c..6eb3a04e3 100644
--- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts
+++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts
@@ -3,6 +3,7 @@ import {
GitHubSyncScope,
GitHubSyncVisibility
} from "@app/hooks/api/secretSyncs/types/github-sync";
+import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
// This functional ensures parity across what is displayed in the destination column
// and the values used when search filtering
@@ -59,6 +60,19 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
case SecretSync.Databricks:
primaryText = destinationConfig.scope;
break;
+ case SecretSync.Humanitec:
+ switch (destinationConfig.scope) {
+ case HumanitecSyncScope.Application:
+ primaryText = destinationConfig.app;
+ break;
+ case HumanitecSyncScope.Environment:
+ primaryText = `${destinationConfig.app} / ${destinationConfig.env}`;
+ break;
+ default:
+ throw new Error(`Unhandled Humanitec Scope Destination Col Values ${destination}`);
+ }
+ secondaryText = `Organization - ${destinationConfig.org}`;
+ break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
index 78a2380d0..34012bb2f 100644
--- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
+++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx
@@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
+import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@@ -49,8 +50,10 @@ import {
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
+ ProjectPermissionDynamicSecretActions,
ProjectPermissionSub,
useProjectPermission,
+ useSubscription,
useWorkspace
} from "@app/context";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
@@ -71,6 +74,7 @@ import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
+import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
@@ -131,6 +135,7 @@ export const OverviewPage = () => {
const [searchFilter, setSearchFilter] = useState("");
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
const secretPath = (routerSearch?.secretPath as string) || "/";
+ const { subscription } = useSubscription();
const [filter, setFilter] = useState(DEFAULT_FILTER_STATE);
const [filterHistory, setFilterHistory] = useState<
@@ -178,6 +183,15 @@ export const OverviewPage = () => {
}, []);
const userAvailableEnvs = currentWorkspace?.environments || [];
+ const userAvailableDynamicSecretEnvs = userAvailableEnvs.filter((env) =>
+ permission.can(
+ ProjectPermissionDynamicSecretActions.CreateRootCredential,
+ subject(ProjectPermissionSub.DynamicSecrets, {
+ environment: env.slug,
+ secretPath
+ })
+ )
+ );
const [visibleEnvs, setVisibleEnvs] = useState(userAvailableEnvs);
@@ -249,7 +263,9 @@ export const OverviewPage = () => {
"addSecretsInAllEnvs",
"addFolder",
"misc",
- "updateFolder"
+ "updateFolder",
+ "addDynamicSecret",
+ "upgradePlan"
] as const);
const handleFolderCreate = async (folderName: string, description: string | null) => {
@@ -851,20 +867,43 @@ export const OverviewPage = () => {
>
{(isAllowed) => (
}
+ leftIcon={}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
- className="h-10"
+ className="h-10 text-left"
isFullWidth
>
Add Folder
)}
+
+ }
+ onClick={() => {
+ if (subscription?.dynamicSecret) {
+ handlePopUpOpen("addDynamicSecret");
+ handlePopUpClose("misc");
+ return;
+ }
+ handlePopUpOpen("upgradePlan");
+ }}
+ isDisabled={userAvailableDynamicSecretEnvs.length === 0}
+ variant="outline_bg"
+ className="h-10 text-left"
+ isFullWidth
+ >
+ Add Dynamic Secret
+
+
@@ -1170,6 +1209,24 @@ export const OverviewPage = () => {
/>
+ handlePopUpToggle("addDynamicSecret", isOpen)}
+ projectSlug={projectSlug}
+ environments={userAvailableDynamicSecretEnvs}
+ secretPath={secretPath}
+ />
+ {subscription && (
+ handlePopUpToggle("upgradePlan", isOpen)}
+ text={
+ subscription.slug === null
+ ? "You can perform this action under an Enterprise license"
+ : "You can perform this action if you switch to Infisical's Team plan"
+ }
+ />
+ )}
);
};
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
index d545ba748..681ad99e3 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx
@@ -655,8 +655,9 @@ export const ActionBar = ({
isOpen={popUp.addDynamicSecret.isOpen}
onToggle={(isOpen) => handlePopUpToggle("addDynamicSecret", isOpen)}
projectSlug={projectSlug}
- environment={environment}
+ environments={[{ slug: environment, name: environment, id: "not-used" }]}
secretPath={secretPath}
+ isSingleEnvironmentMode
/>
24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -59,15 +62,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const AwsElastiCacheInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -87,13 +92,20 @@ export const AwsElastiCacheInputForm = ({
revocationStatement: `{
"UserId": "{{username}}"
}`
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -104,7 +116,7 @@ export const AwsElastiCacheInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -299,6 +311,28 @@ export const AwsElastiCacheInputForm = ({
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx
index 1acb24e8f..f2458bbf5 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AwsIamInputForm.tsx
@@ -5,9 +5,10 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input, TextArea } from "@app/components/v2";
+import { Button, FilterableSelect, FormControl, Input, TextArea } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -40,7 +41,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -49,29 +51,41 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const AwsIamInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
formState: { isSubmitting },
handleSubmit
} = useForm({
- resolver: zodResolver(formSchema)
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
+ }
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
+
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.AwsIam, inputs: provider },
@@ -80,7 +94,7 @@ export const AwsIamInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -286,6 +300,29 @@ export const AwsIamInputForm = ({
)}
/>
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx
index 55cebe173..2fb68f399 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx
@@ -12,7 +12,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input } from "@app/components/v2";
+import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import {
DropdownMenu,
DropdownMenuContent,
@@ -23,6 +23,7 @@ import { Tooltip } from "@app/components/v2/Tooltip";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
selectedUsers: z.array(
@@ -60,7 +61,8 @@ const formSchema = z.object({
name: z
.string()
.min(1)
- .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -69,15 +71,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const AzureEntraIdInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -85,7 +89,10 @@ export const AzureEntraIdInputForm = ({
watch,
handleSubmit
} = useForm({
- resolver: zodResolver(formSchema)
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
+ }
});
const tenantId = watch("provider.tenantId");
const applicationId = watch("provider.applicationId");
@@ -107,7 +114,8 @@ export const AzureEntraIdInputForm = ({
selectedUsers,
provider,
maxTTL,
- defaultTTL
+ defaultTTL,
+ environment
}: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
@@ -129,7 +137,7 @@ export const AzureEntraIdInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
});
onCompleted();
@@ -373,6 +381,29 @@ export const AzureEntraIdInputForm = ({
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx
index c8389fb38..106a658d4 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx
@@ -17,6 +17,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Modal, ModalContent } from "@app/components/v2";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
import { AwsIamInputForm } from "./AwsIamInputForm";
@@ -38,8 +39,9 @@ type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
secretPath: string;
+ isSingleEnvironmentMode?: boolean;
};
enum WizardSteps {
@@ -129,8 +131,9 @@ export const CreateDynamicSecretForm = ({
isOpen,
onToggle,
projectSlug,
- environment,
- secretPath
+ environments,
+ secretPath,
+ isSingleEnvironmentMode
}: Props) => {
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider);
const [selectedProvider, setSelectedProvider] = useState(null);
@@ -197,7 +200,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -215,7 +219,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -233,7 +238,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -251,7 +257,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -269,7 +276,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -287,7 +295,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -305,7 +314,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -323,7 +333,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -341,7 +352,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -359,7 +371,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -377,7 +390,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -395,7 +409,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -413,7 +428,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -432,7 +448,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
@@ -450,7 +467,8 @@ export const CreateDynamicSecretForm = ({
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
- environment={environment}
+ environments={environments}
+ isSingleEnvironmentMode={isSingleEnvironmentMode}
/>
)}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx
index eb0a7008f..a7c84cb50 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/ElasticSearchInputForm.tsx
@@ -9,6 +9,7 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
+ FilterableSelect,
FormControl,
FormLabel,
IconButton,
@@ -19,6 +20,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const authMethods = [
{
@@ -73,7 +75,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -82,15 +85,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const ElasticSearchInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -107,13 +112,20 @@ export const ElasticSearchInputForm = ({
},
roles: ["superuser"],
port: 443
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -124,7 +136,7 @@ export const ElasticSearchInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -406,6 +418,29 @@ export const ElasticSearchInputForm = ({
)}
/>
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx
index ef74db5b8..18f8c62dc 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ Input,
+ Select,
+ SelectItem,
+ TextArea
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
enum CredentialType {
Dynamic = "dynamic",
@@ -69,7 +78,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -79,7 +89,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const LdapInputForm = ({
@@ -87,7 +98,8 @@ export const LdapInputForm = ({
onCancel,
secretPath,
projectSlug,
- environment
+ environments,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -107,7 +119,8 @@ export const LdapInputForm = ({
revocationLdif: "",
rollbackLdif: "",
credentialType: CredentialType.Dynamic
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -115,7 +128,13 @@ export const LdapInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -126,7 +145,7 @@ export const LdapInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -369,6 +388,29 @@ export const LdapInputForm = ({
)}
/>
)}
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx
index 4de9b8acc..821802954 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoAtlasInputForm.tsx
@@ -13,6 +13,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
FormLabel,
IconButton,
@@ -22,6 +23,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -63,7 +65,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -72,7 +75,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
const ATLAS_SCOPE_TYPES = [
@@ -93,9 +97,10 @@ const ATLAS_SCOPE_TYPES = [
export const MongoAtlasInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -108,7 +113,8 @@ export const MongoAtlasInputForm = ({
defaultValues: {
provider: {
roles: [{ databaseName: "", roleName: "" }]
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -124,7 +130,13 @@ export const MongoAtlasInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -135,7 +147,7 @@ export const MongoAtlasInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -438,6 +450,29 @@ export const MongoAtlasInputForm = ({
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx
index b2418f629..789338c87 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ FormLabel,
+ IconButton,
+ Input,
+ SecretInput
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -46,7 +55,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -55,15 +65,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const MongoDBDatabaseInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -76,7 +88,8 @@ export const MongoDBDatabaseInputForm = ({
defaultValues: {
provider: {
roles: [{ roleName: "readWrite" }]
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -87,7 +100,13 @@ export const MongoDBDatabaseInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -105,7 +124,7 @@ export const MongoDBDatabaseInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -321,6 +340,29 @@ export const MongoDBDatabaseInputForm = ({
)}
/>
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx
index 21f8b40de..b593730cf 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx
@@ -7,9 +7,18 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, FormLabel, IconButton, Input, SecretInput } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ FormLabel,
+ IconButton,
+ Input,
+ SecretInput
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +59,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -59,15 +69,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const RabbitMqInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -89,13 +101,20 @@ export const RabbitMqInputForm = ({
}
},
tags: []
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -106,7 +125,7 @@ export const RabbitMqInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -405,6 +424,29 @@ export const RabbitMqInputForm = ({
)}
/>
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx
index 9d9933b44..cec85381e 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/RedisInputForm.tsx
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -59,15 +62,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const RedisInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -81,13 +86,20 @@ export const RedisInputForm = ({
port: 6379,
creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all",
revocationStatement: "ACL DELUSER {{username}}"
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -98,7 +110,7 @@ export const RedisInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -313,6 +325,29 @@ export const RedisInputForm = ({
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx
index efe6e0269..e1ebebc6f 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapAseInputForm.tsx
@@ -11,12 +11,14 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -48,7 +50,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -57,15 +60,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const SapAseInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -82,13 +87,20 @@ sp_adduser '{{username}}', '{{username}}', null;
sp_role 'grant', 'mon_role', '{{username}}';`,
revocationStatement: `sp_dropuser '{{username}}';
sp_droplogin '{{username}}';`
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -99,7 +111,7 @@ sp_droplogin '{{username}}';`
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -291,6 +303,29 @@ sp_droplogin '{{username}}';`
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx
index 1b0bd64b8..27dd20c81 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SapHanaInputForm.tsx
@@ -11,6 +11,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -18,6 +19,7 @@ import {
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -50,7 +52,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -59,15 +62,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const SapHanaInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -82,13 +87,20 @@ GRANT "MONITORING" TO {{username}};`,
revocationStatement: `REVOKE "MONITORING" FROM {{username}};
DROP USER {{username}};`,
renewStatement: "ALTER USER {{username}} VALID UNTIL '{{expiration}}';"
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -99,7 +111,7 @@ DROP USER {{username}};`,
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -311,6 +323,29 @@ DROP USER {{username}};`,
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx
index a5f8b25d9..33e0b9ce5 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SnowflakeInputForm.tsx
@@ -13,12 +13,14 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
TextArea
} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const formSchema = z.object({
provider: z.object({
@@ -54,7 +56,8 @@ const formSchema = z.object({
.string()
.trim()
.min(1)
- .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -63,15 +66,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const SnowflakeInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -85,13 +90,20 @@ export const SnowflakeInputForm = ({
"CREATE USER {{username}} PASSWORD = '{{password}}' DEFAULT_ROLE = public DEFAULT_SECONDARY_ROLES = ('ALL') MUST_CHANGE_PASSWORD = FALSE DAYS_TO_EXPIRY = {{expiration}};",
revocationStatement: "DROP USER {{username}};",
renewStatement: "ALTER USER {{username}} SET DAYS_TO_EXPIRY = {{expiration}};"
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -102,7 +114,7 @@ export const SnowflakeInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch (err) {
@@ -314,6 +326,29 @@ export const SnowflakeInputForm = ({
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
index 4af92b210..732713a1a 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx
@@ -12,6 +12,7 @@ import {
AccordionItem,
AccordionTrigger,
Button,
+ FilterableSelect,
FormControl,
Input,
SecretInput,
@@ -22,6 +23,7 @@ import {
import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
const passwordRequirementsSchema = z
.object({
@@ -79,7 +81,8 @@ const formSchema = z.object({
if (valMs > 24 * 60 * 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
}),
- name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -88,7 +91,8 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
const getSqlStatements = (provider: SqlProviders) => {
@@ -145,9 +149,10 @@ const getDefaultPort = (provider: SqlProviders) => {
export const SqlDatabaseInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const { currentWorkspace } = useWorkspace();
@@ -172,7 +177,8 @@ export const SqlDatabaseInputForm = ({
},
allowedSymbols: "-_.~!*"
}
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -181,9 +187,16 @@ export const SqlDatabaseInputForm = ({
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
- const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
+ const handleCreateDynamicSecret = async ({
+ name,
+ maxTTL,
+ provider,
+ defaultTTL,
+ environment
+ }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
+
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider },
@@ -192,7 +205,7 @@ export const SqlDatabaseInputForm = ({
path: secretPath,
defaultTTL,
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch {
@@ -649,6 +662,29 @@ export const SqlDatabaseInputForm = ({
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx
index 01d5398f0..e1423c111 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm/TotpInputForm.tsx
@@ -5,9 +5,17 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
+import {
+ Button,
+ FilterableSelect,
+ FormControl,
+ Input,
+ Select,
+ SelectItem
+} from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
+import { WorkspaceEnv } from "@app/hooks/api/types";
enum ConfigType {
URL = "url",
@@ -52,7 +60,8 @@ const formSchema = z.object({
.string()
.trim()
.min(1)
- .refine((val) => val.toLowerCase() === val, "Must be lowercase")
+ .refine((val) => val.toLowerCase() === val, "Must be lowercase"),
+ environment: z.object({ name: z.string(), slug: z.string() })
});
type TForm = z.infer;
@@ -62,15 +71,17 @@ type Props = {
onCancel: () => void;
secretPath: string;
projectSlug: string;
- environment: string;
+ environments: WorkspaceEnv[];
+ isSingleEnvironmentMode?: boolean;
};
export const TotpInputForm = ({
onCompleted,
onCancel,
- environment,
+ environments,
secretPath,
- projectSlug
+ projectSlug,
+ isSingleEnvironmentMode
}: Props) => {
const {
control,
@@ -82,7 +93,8 @@ export const TotpInputForm = ({
defaultValues: {
provider: {
configType: ConfigType.URL
- }
+ },
+ environment: isSingleEnvironmentMode ? environments[0] : undefined
}
});
@@ -90,7 +102,7 @@ export const TotpInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
- const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
+ const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isPending) return;
try {
@@ -101,7 +113,7 @@ export const TotpInputForm = ({
path: secretPath,
defaultTTL: "1m",
projectSlug,
- environmentSlug: environment
+ environmentSlug: environment.slug
});
onCompleted();
} catch (err) {
@@ -295,6 +307,29 @@ export const TotpInputForm = ({
>
)}
+ {!isSingleEnvironmentMode && (
+ (
+
+ option.name}
+ getOptionValue={(option) => option.slug}
+ menuPlacement="top"
+ />
+
+ )}
+ />
+ )}
diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
index 629c23048..01d477097 100644
--- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
+++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem.tsx
@@ -306,7 +306,10 @@ export const SecretItem = memo(
)}
/>
)}
-
+
{
+ const { destinationConfig } = secretSync;
+
+ let Components: ReactNode;
+ switch (destinationConfig.scope) {
+ case HumanitecSyncScope.Application:
+ Components = (
+ <>
+ {destinationConfig.app}
+ {destinationConfig.org}
+ >
+ );
+ break;
+ case HumanitecSyncScope.Environment:
+ Components = (
+ <>
+ {destinationConfig.app}
+ {destinationConfig.org}
+ {destinationConfig.env}
+ >
+ );
+ break;
+ default:
+ throw new Error(
+ `Uhandled Humanitec Sync Destination Section Scope ${secretSync.destinationConfig.scope}`
+ );
+ }
+
+ return (
+ <>
+
+ {destinationConfig.scope.replace("-", " ")}
+
+ {Components}
+ >
+ );
+};
diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx
index b7d3964c7..c1dee10c2 100644
--- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx
+++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx
@@ -17,6 +17,7 @@ import { GitHubSyncDestinationSection } from "@app/pages/secret-manager/SecretSy
import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection";
import { AzureKeyVaultSyncDestinationSection } from "./AzureKeyVaultSyncDestinationSection";
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
+import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
type Props = {
secretSync: TSecretSync;
@@ -53,6 +54,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Databricks:
DestinationComponents = ;
break;
+ case SecretSync.Humanitec:
+ DestinationComponents = ;
+ break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx
index df6927b48..351cb70e1 100644
--- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx
+++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
-import { IconButton } from "@app/components/v2";
+import { Badge, IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP } from "@app/helpers/secretSyncs";
@@ -24,7 +24,8 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
syncOptions: {
// appendSuffix,
// prependPrefix,
- initialSyncBehavior
+ initialSyncBehavior,
+ disableSecretDeletion
}
} = secretSync;
@@ -46,6 +47,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.AzureKeyVault:
case SecretSync.AzureAppConfiguration:
case SecretSync.Databricks:
+ case SecretSync.Humanitec:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -57,24 +59,22 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
Sync Options
- {AdditionalSyncOptionsComponent && (
-
- {(isAllowed) => (
-
-
-
- )}
-
- )}
+
+ {(isAllowed) => (
+
+
+
+ )}
+
@@ -84,6 +84,11 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
{/* {prependPrefix}
{appendSuffix} */}
{AdditionalSyncOptionsComponent}
+ {disableSecretDeletion && (
+
+ Disabled
+
+ )}
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index e6bd0c8ee..ef31a5a33 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -42,7 +42,6 @@ import { Route as authProviderSuccessPageRouteImport } from './pages/auth/Provid
import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route'
import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route'
import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route'
-import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route'
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
@@ -54,6 +53,7 @@ import { Route as secretManagerLayoutImport } from './pages/secret-manager/layou
import { Route as kmsLayoutImport } from './pages/kms/layout'
import { Route as certManagerLayoutImport } from './pages/cert-manager/layout'
import { Route as organizationSshOverviewPageRouteImport } from './pages/organization/SshOverviewPage/route'
+import { Route as organizationSecretSharingSettingsPageRouteImport } from './pages/organization/SecretSharingSettingsPage/route'
import { Route as organizationSecretManagerOverviewPageRouteImport } from './pages/organization/SecretManagerOverviewPage/route'
import { Route as organizationRoleByIDPageRouteImport } from './pages/organization/RoleByIDPage/route'
import { Route as organizationUserDetailsByIDPageRouteImport } from './pages/organization/UserDetailsByIDPage/route'
@@ -61,6 +61,7 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
+import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
@@ -211,6 +212,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
)()
+const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport =
+ createFileRoute(
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing',
+ )()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/gateways',
@@ -467,6 +472,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
+const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute =
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport.update({
+ id: '/secret-sharing',
+ path: '/secret-sharing',
+ getParentRoute: () =>
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
+ } as any)
+
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport.update({
id: '/gateways',
@@ -505,14 +518,6 @@ const organizationSettingsPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
-const organizationSecretSharingPageRouteRoute =
- organizationSecretSharingPageRouteImport.update({
- id: '/secret-sharing',
- path: '/secret-sharing',
- getParentRoute: () =>
- AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
- } as any)
-
const organizationSecretScanningPageRouteRoute =
organizationSecretScanningPageRouteImport.update({
id: '/secret-scanning',
@@ -590,6 +595,14 @@ const organizationSshOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
+const organizationSecretSharingSettingsPageRouteRoute =
+ organizationSecretSharingSettingsPageRouteImport.update({
+ id: '/settings',
+ path: '/settings',
+ getParentRoute: () =>
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
+ } as any)
+
const organizationSecretManagerOverviewPageRouteRoute =
organizationSecretManagerOverviewPageRouteImport.update({
id: '/secret-manager/overview',
@@ -646,6 +659,14 @@ const organizationCertManagerOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
+const organizationSecretSharingPageRouteRoute =
+ organizationSecretSharingPageRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () =>
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
+ } as any)
+
const organizationGatewaysGatewayListPageRouteRoute =
organizationGatewaysGatewayListPageRouteImport.update({
id: '/',
@@ -1887,13 +1908,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSecretScanningPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
- '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': {
- id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
- path: '/secret-sharing'
- fullPath: '/organization/secret-sharing'
- preLoaderRoute: typeof organizationSecretSharingPageRouteImport
- parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
- }
'/_authenticate/_inject-org-details/_org-layout/organization/settings': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/settings'
path: '/settings'
@@ -1929,6 +1943,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': {
+ id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
+ path: '/secret-sharing'
+ fullPath: '/organization/secret-sharing'
+ preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport
+ parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
+ }
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
path: '/secret-manager/$projectId'
@@ -1957,6 +1978,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationGatewaysGatewayListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
}
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': {
+ id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
+ path: '/'
+ fullPath: '/organization/secret-sharing/'
+ preLoaderRoute: typeof organizationSecretSharingPageRouteImport
+ parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport
+ }
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
path: '/cert-manager/overview'
@@ -2006,6 +2034,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSecretManagerOverviewPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': {
+ id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings'
+ path: '/settings'
+ fullPath: '/organization/secret-sharing/settings'
+ preLoaderRoute: typeof organizationSecretSharingSettingsPageRouteImport
+ parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport
+ }
'/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview'
path: '/ssh/overview'
@@ -2965,16 +3000,34 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren,
)
+interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren {
+ organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
+ organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute
+}
+
+const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren =
+ {
+ organizationSecretSharingPageRouteRoute:
+ organizationSecretSharingPageRouteRoute,
+ organizationSecretSharingSettingsPageRouteRoute:
+ organizationSecretSharingSettingsPageRouteRoute,
+ }
+
+const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren =
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute._addFileChildren(
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren,
+ )
+
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute
- organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -2994,13 +3047,13 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
organizationSecretScanningPageRouteRoute:
organizationSecretScanningPageRouteRoute,
- organizationSecretSharingPageRouteRoute:
- organizationSecretSharingPageRouteRoute,
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
+ AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
organizationCertManagerOverviewPageRouteRoute:
organizationCertManagerOverviewPageRouteRoute,
organizationGroupDetailsByIDPageRouteRoute:
@@ -3672,16 +3725,17 @@ export interface FileRoutesByFullPath {
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
- '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
+ '/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
+ '/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -3689,6 +3743,7 @@ export interface FileRoutesByFullPath {
'/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute
+ '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute
'/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
@@ -3845,7 +3900,6 @@ export interface FileRoutesByTo {
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/organization/billing': typeof organizationBillingPageRouteRoute
'/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
- '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
@@ -3853,6 +3907,7 @@ export interface FileRoutesByTo {
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
+ '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -3860,6 +3915,7 @@ export interface FileRoutesByTo {
'/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute
+ '/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute
'/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
@@ -4025,16 +4081,17 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute
- '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof organizationSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@@ -4042,6 +4099,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId': typeof organizationUserDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId': typeof organizationRoleByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview': typeof organizationSecretManagerOverviewPageRouteRoute
+ '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings': typeof organizationSecretSharingSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview': typeof organizationSshOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout': typeof certManagerLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout': typeof kmsLayoutRouteWithChildren
@@ -4208,16 +4266,17 @@ export interface FileRouteTypes {
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/secret-scanning'
- | '/organization/secret-sharing'
| '/organization/settings'
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/organization/app-connections'
| '/organization/gateways'
+ | '/organization/secret-sharing'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections/'
| '/organization/gateways/'
+ | '/organization/secret-sharing/'
| '/organization/cert-manager/overview'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
@@ -4225,6 +4284,7 @@ export interface FileRouteTypes {
| '/organization/members/$membershipId'
| '/organization/roles/$roleId'
| '/organization/secret-manager/overview'
+ | '/organization/secret-sharing/settings'
| '/organization/ssh/overview'
| '/cert-manager/$projectId/overview'
| '/cert-manager/$projectId/settings'
@@ -4380,7 +4440,6 @@ export interface FileRouteTypes {
| '/organization/audit-logs'
| '/organization/billing'
| '/organization/secret-scanning'
- | '/organization/secret-sharing'
| '/organization/settings'
| '/cert-manager/$projectId'
| '/kms/$projectId'
@@ -4388,6 +4447,7 @@ export interface FileRouteTypes {
| '/ssh/$projectId'
| '/organization/app-connections'
| '/organization/gateways'
+ | '/organization/secret-sharing'
| '/organization/cert-manager/overview'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
@@ -4395,6 +4455,7 @@ export interface FileRouteTypes {
| '/organization/members/$membershipId'
| '/organization/roles/$roleId'
| '/organization/secret-manager/overview'
+ | '/organization/secret-sharing/settings'
| '/organization/ssh/overview'
| '/cert-manager/$projectId/overview'
| '/cert-manager/$projectId/settings'
@@ -4558,16 +4619,17 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
| '/_authenticate/_inject-org-details/_org-layout/organization/billing'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning'
- | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
+ | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
+ | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
@@ -4575,6 +4637,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/organization/members/$membershipId'
| '/_authenticate/_inject-org-details/_org-layout/organization/roles/$roleId'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/overview'
+ | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings'
| '/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout'
@@ -4936,10 +4999,10 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning",
- "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
@@ -4981,10 +5044,6 @@ export const routeTree = rootRoute
"filePath": "organization/SecretScanningPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
- "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": {
- "filePath": "organization/SecretSharingPage/route.tsx",
- "parent": "/_authenticate/_inject-org-details/_org-layout/organization"
- },
"/_authenticate/_inject-org-details/_org-layout/organization/settings": {
"filePath": "organization/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
@@ -5018,6 +5077,14 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
]
},
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": {
+ "filePath": "",
+ "parent": "/_authenticate/_inject-org-details/_org-layout/organization",
+ "children": [
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/",
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
+ ]
+ },
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@@ -5040,6 +5107,10 @@ export const routeTree = rootRoute
"filePath": "organization/Gateways/GatewayListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/gateways"
},
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/": {
+ "filePath": "organization/SecretSharingPage/route.tsx",
+ "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
+ },
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
"filePath": "organization/CertManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
@@ -5068,6 +5139,10 @@ export const routeTree = rootRoute
"filePath": "organization/SecretManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
},
+ "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings": {
+ "filePath": "organization/SecretSharingSettingsPage/route.tsx",
+ "parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
+ },
"/_authenticate/_inject-org-details/_org-layout/organization/ssh/overview": {
"filePath": "organization/SshOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 983129e33..19cf8fc62 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -16,7 +16,10 @@ const organizationRoutes = route("/organization", [
route("/admin", "organization/AdminPage/route.tsx"),
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
route("/billing", "organization/BillingPage/route.tsx"),
- route("/secret-sharing", "organization/SecretSharingPage/route.tsx"),
+ route("/secret-sharing", [
+ index("organization/SecretSharingPage/route.tsx"),
+ route("/settings", "organization/SecretSharingSettingsPage/route.tsx")
+ ]),
route("/settings", "organization/SettingsPage/route.tsx"),
route("/secret-scanning", "organization/SecretScanningPage/route.tsx"),
route("/groups/$groupId", "organization/GroupDetailsByIDPage/route.tsx"),
diff --git a/helm-charts/infisical-standalone-postgres/CHANGELOG.md b/helm-charts/infisical-standalone-postgres/CHANGELOG.md
index 80b3d7fcc..342510a5c 100644
--- a/helm-charts/infisical-standalone-postgres/CHANGELOG.md
+++ b/helm-charts/infisical-standalone-postgres/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.4.1 (March 19, 2025)
+
+Changes:
+* Added support for supplying extra volume mounts and volumes via `infisical.extraVolumeMounts` and `infisical.extraVolumes`
+
## 1.4.0 (November 06, 2024)
Changes:
diff --git a/helm-charts/infisical-standalone-postgres/Chart.yaml b/helm-charts/infisical-standalone-postgres/Chart.yaml
index a4cbc49c7..6c042e841 100644
--- a/helm-charts/infisical-standalone-postgres/Chart.yaml
+++ b/helm-charts/infisical-standalone-postgres/Chart.yaml
@@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 1.4.0
+version: 1.4.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
diff --git a/helm-charts/infisical-standalone-postgres/templates/infisical.yaml b/helm-charts/infisical-standalone-postgres/templates/infisical.yaml
index ac941c9b2..8684014d6 100644
--- a/helm-charts/infisical-standalone-postgres/templates/infisical.yaml
+++ b/helm-charts/infisical-standalone-postgres/templates/infisical.yaml
@@ -77,6 +77,14 @@ spec:
{{- if $infisicalValues.resources }}
resources: {{- toYaml $infisicalValues.resources | nindent 12 }}
{{- end }}
+ {{- with $infisicalValues.extraVolumeMounts }}
+ volumeMounts:
+ {{- toYaml . | nindent 10 }}
+ {{- end }}
+ {{- with $infisicalValues.extraVolumes }}
+ volumes:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
---
apiVersion: v1
diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml
index ada69a80e..84d926623 100644
--- a/helm-charts/secrets-operator/Chart.yaml
+++ b/helm-charts/secrets-operator/Chart.yaml
@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: v0.8.12
+version: v0.8.14
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
-appVersion: "v0.8.12"
+appVersion: "v0.8.14"
diff --git a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml
index ce4a00c46..1dff23eea 100644
--- a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml
+++ b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml
@@ -262,6 +262,43 @@ spec:
hostAPI:
description: Infisical host to pull secrets from
type: string
+ managedKubeConfigMapReferences:
+ items:
+ properties:
+ configMapName:
+ description: The name of the Kubernetes ConfigMap
+ type: string
+ configMapNamespace:
+ description: The namespace where the Kubernetes ConfigMap is located
+ type: string
+ creationPolicy:
+ default: Orphan
+ description: 'The Kubernetes ConfigMap creation policy. Enum with
+ values: ''Owner'', ''Orphan''. Owner creates the config map
+ and sets .metadata.ownerReferences of the InfisicalSecret CRD
+ that created it. Orphan will not set the config map owner. This
+ will result in the config map being orphaned and not deleted
+ when the resource is deleted.'
+ type: string
+ template:
+ description: The template to transform the secret data
+ properties:
+ data:
+ additionalProperties:
+ type: string
+ description: The template key values
+ type: object
+ includeAllSecrets:
+ description: This injects all retrieved secrets into the top
+ level of your template. Secrets defined in the template
+ will take precedence over the injected ones.
+ type: boolean
+ type: object
+ required:
+ - configMapName
+ - configMapNamespace
+ type: object
+ type: array
managedKubeSecretReferences:
items:
properties:
@@ -380,6 +417,7 @@ spec:
- secretNamespace
type: object
required:
+ - managedKubeConfigMapReferences
- resyncInterval
type: object
status:
@@ -467,4 +505,5 @@ status:
plural: ""
conditions: []
storedVersions: []
+
{{- end }}
\ No newline at end of file
diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml
index 6a23a8926..b38199000 100644
--- a/helm-charts/secrets-operator/values.yaml
+++ b/helm-charts/secrets-operator/values.yaml
@@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
- tag: v0.8.12
+ tag: v0.8.14
resources:
limits:
cpu: 500m
diff --git a/k8-operator/api/v1alpha1/common.go b/k8-operator/api/v1alpha1/common.go
index f9197bc8b..5631984d1 100644
--- a/k8-operator/api/v1alpha1/common.go
+++ b/k8-operator/api/v1alpha1/common.go
@@ -107,3 +107,25 @@ type ManagedKubeSecretConfig struct {
// +kubebuilder:validation:Optional
Template *InfisicalSecretTemplate `json:"template,omitempty"`
}
+
+type ManagedKubeConfigMapConfig struct {
+ // The name of the Kubernetes ConfigMap
+ // +kubebuilder:validation:Required
+ ConfigMapName string `json:"configMapName"`
+
+ // The Kubernetes ConfigMap creation policy.
+ // Enum with values: 'Owner', 'Orphan'.
+ // Owner creates the config map and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
+ // Orphan will not set the config map owner. This will result in the config map being orphaned and not deleted when the resource is deleted.
+ // +kubebuilder:validation:Optional
+ // +kubebuilder:default:=Orphan
+ CreationPolicy string `json:"creationPolicy"`
+
+ // The namespace where the Kubernetes ConfigMap is located
+ // +kubebuilder:validation:Required
+ ConfigMapNamespace string `json:"configMapNamespace"`
+
+ // The template to transform the secret data
+ // +kubebuilder:validation:Optional
+ Template *InfisicalSecretTemplate `json:"template,omitempty"`
+}
diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go
index c8f805074..8f871dfa6 100644
--- a/k8-operator/api/v1alpha1/infisicalsecret_types.go
+++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go
@@ -139,6 +139,8 @@ type InfisicalSecretSpec struct {
// +kubebuilder:validation:Optional
ManagedKubeSecretReferences []ManagedKubeSecretConfig `json:"managedKubeSecretReferences"`
+ // +kubebuilder:validation:Optional
+ ManagedKubeConfigMapReferences []ManagedKubeConfigMapConfig `json:"managedKubeConfigMapReferences"`
// +kubebuilder:default:=60
ResyncInterval int `json:"resyncInterval"`
diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go
index 388ad8497..11d864ad4 100644
--- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go
+++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go
@@ -572,6 +572,13 @@ func (in *InfisicalSecretSpec) DeepCopyInto(out *InfisicalSecretSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.ManagedKubeConfigMapReferences != nil {
+ in, out := &in.ManagedKubeConfigMapReferences, &out.ManagedKubeConfigMapReferences
+ *out = make([]ManagedKubeConfigMapConfig, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
out.TLS = in.TLS
}
@@ -691,6 +698,26 @@ func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWor
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ManagedKubeConfigMapConfig) DeepCopyInto(out *ManagedKubeConfigMapConfig) {
+ *out = *in
+ if in.Template != nil {
+ in, out := &in.Template, &out.Template
+ *out = new(InfisicalSecretTemplate)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedKubeConfigMapConfig.
+func (in *ManagedKubeConfigMapConfig) DeepCopy() *ManagedKubeConfigMapConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(ManagedKubeConfigMapConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ManagedKubeSecretConfig) DeepCopyInto(out *ManagedKubeSecretConfig) {
*out = *in
diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml
index 27ae55ba0..49ed43a77 100644
--- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml
+++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml
@@ -261,6 +261,44 @@ spec:
hostAPI:
description: Infisical host to pull secrets from
type: string
+ managedKubeConfigMapReferences:
+ items:
+ properties:
+ configMapName:
+ description: The name of the Kubernetes ConfigMap
+ type: string
+ configMapNamespace:
+ description: The namespace where the Kubernetes ConfigMap is
+ located
+ type: string
+ creationPolicy:
+ default: Orphan
+ description: 'The Kubernetes ConfigMap creation policy. Enum
+ with values: ''Owner'', ''Orphan''. Owner creates the config
+ map and sets .metadata.ownerReferences of the InfisicalSecret
+ CRD that created it. Orphan will not set the config map owner.
+ This will result in the config map being orphaned and not
+ deleted when the resource is deleted.'
+ type: string
+ template:
+ description: The template to transform the secret data
+ properties:
+ data:
+ additionalProperties:
+ type: string
+ description: The template key values
+ type: object
+ includeAllSecrets:
+ description: This injects all retrieved secrets into the
+ top level of your template. Secrets defined in the template
+ will take precedence over the injected ones.
+ type: boolean
+ type: object
+ required:
+ - configMapName
+ - configMapNamespace
+ type: object
+ type: array
managedKubeSecretReferences:
items:
properties:
diff --git a/k8-operator/controllers/infisicalsecret/conditions.go b/k8-operator/controllers/infisicalsecret/conditions.go
index 7c9aa6218..b9d09d5e2 100644
--- a/k8-operator/controllers/infisicalsecret/conditions.go
+++ b/k8-operator/controllers/infisicalsecret/conditions.go
@@ -11,7 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
-func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount int, errorToConditionOn error) error {
+func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount int, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
@@ -39,7 +39,10 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.
})
}
- return r.Client.Status().Update(ctx, infisicalSecret)
+ err := r.Client.Status().Update(ctx, infisicalSecret)
+ if err != nil {
+ logger.Error(err, "Could not set condition for ReadyToSyncSecrets")
+ }
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, authStrategy util.AuthStrategyType, errorToConditionOn error) {
diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go
index b4765d6e1..301f3856f 100644
--- a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go
+++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go
@@ -73,6 +73,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// It's important we don't directly modify the CRD object, so we create a copy of it and move existing data into it.
managedKubeSecretReferences := infisicalSecretCRD.Spec.ManagedKubeSecretReferences
+ managedKubeConfigMapReferences := infisicalSecretCRD.Spec.ManagedKubeConfigMapReferences
if infisicalSecretCRD.Spec.ManagedSecretReference.SecretName != "" && managedKubeSecretReferences != nil && len(managedKubeSecretReferences) > 0 {
errMessage := "InfisicalSecret CRD cannot have both managedSecretReference and managedKubeSecretReferences"
@@ -89,8 +90,8 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
managedKubeSecretReferences = append(managedKubeSecretReferences, infisicalSecretCRD.Spec.ManagedSecretReference)
}
- if len(managedKubeSecretReferences) == 0 {
- errMessage := "InfisicalSecret CRD must have at least one managed secret reference set in the `managedKubeSecretReferences` field"
+ if len(managedKubeSecretReferences) == 0 && len(managedKubeConfigMapReferences) == 0 {
+ errMessage := "InfisicalSecret CRD must have at least one managed secret reference set in the `managedKubeSecretReferences` or `managedKubeConfigMapReferences` field"
logger.Error(defaultErrors.New(errMessage), errMessage)
return ctrl.Result{}, defaultErrors.New(errMessage)
}
@@ -151,8 +152,8 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
api.API_CA_CERTIFICATE = ""
}
- secretsCount, err := r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
- r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, secretsCount, err)
+ secretsCount, err := r.ReconcileInfisicalSecret(ctx, logger, &infisicalSecretCRD, managedKubeSecretReferences, managedKubeConfigMapReferences)
+ r.SetReadyToSyncSecretsConditions(ctx, logger, &infisicalSecretCRD, secretsCount, err)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime))
@@ -163,6 +164,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
numDeployments, err := controllerhelpers.ReconcileDeploymentsWithMultipleManagedSecrets(ctx, r.Client, logger, managedKubeSecretReferences)
r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err)
+
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go
index f3800f955..eb41fe6e9 100644
--- a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go
+++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go
@@ -12,6 +12,7 @@ import (
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/constants"
+ "github.com/Infisical/infisical/k8-operator/packages/crypto"
"github.com/Infisical/infisical/k8-operator/packages/model"
"github.com/Infisical/infisical/k8-operator/packages/util"
"github.com/go-logr/logr"
@@ -165,10 +166,24 @@ var infisicalSecretTemplateFunctions = template.FuncMap{
},
}
-func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedSecretReference v1alpha1.ManagedKubeSecretConfig, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
+func convertBinaryToStringMap(binaryMap map[string][]byte) map[string]string {
+ stringMap := make(map[string]string)
+ for k, v := range binaryMap {
+ stringMap[k] = string(v)
+ }
+ return stringMap
+}
+
+func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedSecretReferenceInterface interface{}, secretsFromAPI []model.SingleEnvironmentVariable, ETag string, resourceType constants.ManagedKubeResourceType) error {
plainProcessedSecrets := make(map[string][]byte)
- secretType := managedSecretReference.SecretType
- managedTemplateData := managedSecretReference.Template
+
+ var managedTemplateData *v1alpha1.InfisicalSecretTemplate
+
+ if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET {
+ managedTemplateData = managedSecretReferenceInterface.(v1alpha1.ManagedKubeSecretConfig).Template
+ } else if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP {
+ managedTemplateData = managedSecretReferenceInterface.(v1alpha1.ManagedKubeConfigMapConfig).Template
+ }
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
for _, secret := range secretsFromAPI {
@@ -221,34 +236,70 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
}
}
- annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
- // create a new secret as specified by the managed secret spec of CRD
- newKubeSecretInstance := &corev1.Secret{
- ObjectMeta: metav1.ObjectMeta{
- Name: managedSecretReference.SecretName,
- Namespace: managedSecretReference.SecretNamespace,
- Annotations: annotations,
- Labels: labels,
- },
- Type: corev1.SecretType(secretType),
- Data: plainProcessedSecrets,
- }
+ if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET {
- if managedSecretReference.CreationPolicy == "Owner" {
- // Set InfisicalSecret instance as the owner and controller of the managed secret
- err := ctrl.SetControllerReference(&infisicalSecret, newKubeSecretInstance, r.Scheme)
- if err != nil {
- return err
+ managedSecretReference := managedSecretReferenceInterface.(v1alpha1.ManagedKubeSecretConfig)
+
+ annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
+ // create a new secret as specified by the managed secret spec of CRD
+ newKubeSecretInstance := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: managedSecretReference.SecretName,
+ Namespace: managedSecretReference.SecretNamespace,
+ Annotations: annotations,
+ Labels: labels,
+ },
+ Type: corev1.SecretType(managedSecretReference.SecretType),
+ Data: plainProcessedSecrets,
}
- }
- err := r.Client.Create(ctx, newKubeSecretInstance)
- if err != nil {
- return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err)
- }
+ if managedSecretReference.CreationPolicy == "Owner" {
+ // Set InfisicalSecret instance as the owner and controller of the managed secret
+ err := ctrl.SetControllerReference(&infisicalSecret, newKubeSecretInstance, r.Scheme)
+ if err != nil {
+ return err
+ }
+ }
+
+ err := r.Client.Create(ctx, newKubeSecretInstance)
+ if err != nil {
+ return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err)
+ }
+ logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s", managedSecretReference.SecretType))
+ return nil
+ } else if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP {
+
+ managedSecretReference := managedSecretReferenceInterface.(v1alpha1.ManagedKubeConfigMapConfig)
+
+ // create a new config map as specified by the managed secret spec of CRD
+ newKubeConfigMapInstance := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: managedSecretReference.ConfigMapName,
+ Namespace: managedSecretReference.ConfigMapNamespace,
+ Annotations: annotations,
+ Labels: labels,
+ },
+ Data: convertBinaryToStringMap(plainProcessedSecrets),
+ }
+
+ if managedSecretReference.CreationPolicy == "Owner" {
+ // Set InfisicalSecret instance as the owner and controller of the managed config map
+ err := ctrl.SetControllerReference(&infisicalSecret, newKubeConfigMapInstance, r.Scheme)
+ if err != nil {
+ return err
+ }
+ }
+
+ err := r.Client.Create(ctx, newKubeConfigMapInstance)
+ if err != nil {
+ return fmt.Errorf("unable to create the managed Kubernetes config map : %w", err)
+ }
+ logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes config map with your Infisical secrets. Type: %s", managedSecretReference.ConfigMapName))
+ return nil
+
+ }
+ return fmt.Errorf("invalid resource type")
- logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s", secretType))
- return nil
}
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, managedSecretReference v1alpha1.ManagedKubeSecretConfig, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
@@ -302,6 +353,109 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context
return nil
}
+func (r *InfisicalSecretReconciler) updateInfisicalManagedConfigMap(ctx context.Context, logger logr.Logger, managedConfigMapReference v1alpha1.ManagedKubeConfigMapConfig, managedConfigMap corev1.ConfigMap, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
+ managedTemplateData := managedConfigMapReference.Template
+
+ plainProcessedSecrets := make(map[string][]byte)
+ if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
+ for _, secret := range secretsFromAPI {
+ plainProcessedSecrets[secret.Key] = []byte(secret.Value)
+ }
+ }
+
+ if managedTemplateData != nil {
+ secretKeyValue := make(map[string]model.SecretTemplateOptions)
+ for _, secret := range secretsFromAPI {
+ secretKeyValue[secret.Key] = model.SecretTemplateOptions{
+ Value: secret.Value,
+ SecretPath: secret.SecretPath,
+ }
+ }
+
+ for templateKey, userTemplate := range managedTemplateData.Data {
+ tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate)
+ if err != nil {
+ return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
+ }
+
+ buf := bytes.NewBuffer(nil)
+ err = tmpl.Execute(buf, secretKeyValue)
+ if err != nil {
+ return fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err)
+ }
+ plainProcessedSecrets[templateKey] = buf.Bytes()
+ }
+ }
+
+ // Initialize the Annotations map if it's nil
+ if managedConfigMap.ObjectMeta.Annotations == nil {
+ managedConfigMap.ObjectMeta.Annotations = make(map[string]string)
+ }
+
+ managedConfigMap.Data = convertBinaryToStringMap(plainProcessedSecrets)
+ managedConfigMap.ObjectMeta.Annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
+
+ err := r.Client.Update(ctx, &managedConfigMap)
+ if err != nil {
+ return fmt.Errorf("unable to update Kubernetes config map because [%w]", err)
+ }
+
+ logger.Info("successfully updated managed Kubernetes config map")
+ return nil
+}
+
+func (r *InfisicalSecretReconciler) fetchSecretsFromAPI(ctx context.Context, logger logr.Logger, authDetails util.AuthenticationDetails, infisicalClient infisicalSdk.InfisicalClientInterface, infisicalSecret v1alpha1.InfisicalSecret) ([]model.SingleEnvironmentVariable, error) {
+
+ if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
+ serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
+ if err != nil {
+ return nil, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
+ }
+
+ plainTextSecretsFromApi, err := util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName)
+ if err != nil {
+ return nil, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
+ }
+
+ logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
+
+ return plainTextSecretsFromApi, nil
+
+ } else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
+ infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
+ if err != nil {
+ return nil, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
+ }
+
+ envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
+ secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
+ recursive := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.Recursive
+
+ plainTextSecretsFromApi, err := util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, envSlug, secretsPath, recursive)
+ if err != nil {
+ return nil, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
+ }
+
+ logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
+
+ return plainTextSecretsFromApi, nil
+
+ } else if authDetails.IsMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point
+ plainTextSecretsFromApi, err := util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, authDetails.MachineIdentityScope)
+
+ if err != nil {
+ return nil, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
+ }
+
+ logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
+
+ return plainTextSecretsFromApi, nil
+
+ } else {
+ return nil, errors.New("no authentication method provided. Please configure a authentication method then try again")
+ }
+}
+
func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha1.InfisicalSecret) util.ResourceVariables {
var resourceVariables util.ResourceVariables
@@ -336,9 +490,13 @@ func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1al
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
}
-func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig) (int, error) {
+func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig, managedKubeConfigMapReferences []v1alpha1.ManagedKubeConfigMapConfig) (int, error) {
- resourceVariables := r.getResourceVariables(infisicalSecret)
+ if infisicalSecret == nil {
+ return 0, fmt.Errorf("infisicalSecret is nil")
+ }
+
+ resourceVariables := r.getResourceVariables(*infisicalSecret)
infisicalClient := resourceVariables.InfisicalClient
cancelCtx := resourceVariables.CancelCtx
authDetails := resourceVariables.AuthDetails
@@ -346,95 +504,74 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
if authDetails.AuthStrategy == "" {
logger.Info("No authentication strategy found. Attempting to authenticate")
- authDetails, err = r.handleAuthentication(ctx, infisicalSecret, infisicalClient)
- r.SetInfisicalTokenLoadCondition(ctx, logger, &infisicalSecret, authDetails.AuthStrategy, err)
+ authDetails, err = r.handleAuthentication(ctx, *infisicalSecret, infisicalClient)
+ r.SetInfisicalTokenLoadCondition(ctx, logger, infisicalSecret, authDetails.AuthStrategy, err)
if err != nil {
return 0, fmt.Errorf("unable to authenticate [err=%s]", err)
}
- r.updateResourceVariables(infisicalSecret, util.ResourceVariables{
+ r.updateResourceVariables(*infisicalSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
})
}
- secretsCount := 0
+ plainTextSecretsFromApi, err := r.fetchSecretsFromAPI(ctx, logger, authDetails, infisicalClient, *infisicalSecret)
- for _, managedSecretReference := range managedKubeSecretReferences {
- // Look for managed secret by name and namespace
- managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
- Name: managedSecretReference.SecretName,
- Namespace: managedSecretReference.SecretNamespace,
- })
+ if err != nil {
+ return 0, fmt.Errorf("failed to fetch secrets from API for managed secrets [err=%s]", err)
+ }
+ secretsCount := len(plainTextSecretsFromApi)
- if err != nil && !k8Errors.IsNotFound(err) {
- return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
+ if len(managedKubeSecretReferences) > 0 {
+ for _, managedSecretReference := range managedKubeSecretReferences {
+ // Look for managed secret by name and namespace
+ managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
+ Name: managedSecretReference.SecretName,
+ Namespace: managedSecretReference.SecretNamespace,
+ })
+
+ if err != nil && !k8Errors.IsNotFound(err) {
+ return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
+ }
+
+ newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", plainTextSecretsFromApi)))
+ if managedKubeSecret == nil {
+ if err := r.createInfisicalManagedKubeResource(ctx, logger, *infisicalSecret, managedSecretReference, plainTextSecretsFromApi, newEtag, constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET); err != nil {
+ return 0, fmt.Errorf("failed to create managed secret [err=%s]", err)
+ }
+ } else {
+ if err := r.updateInfisicalManagedKubeSecret(ctx, logger, managedSecretReference, *managedKubeSecret, plainTextSecretsFromApi, newEtag); err != nil {
+ return 0, fmt.Errorf("failed to update managed secret [err=%s]", err)
+ }
+ }
}
+ }
- // Get exiting Etag if exists
- secretVersionBasedOnETag := ""
- if managedKubeSecret != nil {
- secretVersionBasedOnETag = managedKubeSecret.Annotations[constants.SECRET_VERSION_ANNOTATION]
- }
+ if len(managedKubeConfigMapReferences) > 0 {
+ for _, managedConfigMapReference := range managedKubeConfigMapReferences {
+ managedKubeConfigMap, err := util.GetKubeConfigMapByNamespacedName(ctx, r.Client, types.NamespacedName{
+ Name: managedConfigMapReference.ConfigMapName,
+ Namespace: managedConfigMapReference.ConfigMapNamespace,
+ })
- var plainTextSecretsFromApi []model.SingleEnvironmentVariable
- var updateDetails model.RequestUpdateUpdateDetails
-
- if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
- serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
- if err != nil {
- return 0, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
+ if err != nil && !k8Errors.IsNotFound(err) {
+ return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes config map [%w]", err)
}
- plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
- if err != nil {
- return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
+ newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", plainTextSecretsFromApi)))
+ if managedKubeConfigMap == nil {
+ if err := r.createInfisicalManagedKubeResource(ctx, logger, *infisicalSecret, managedConfigMapReference, plainTextSecretsFromApi, newEtag, constants.MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP); err != nil {
+ return 0, fmt.Errorf("failed to create managed config map [err=%s]", err)
+ }
+ } else {
+ if err := r.updateInfisicalManagedConfigMap(ctx, logger, managedConfigMapReference, *managedKubeConfigMap, plainTextSecretsFromApi, newEtag); err != nil {
+ return 0, fmt.Errorf("failed to update managed config map [err=%s]", err)
+ }
}
- logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
-
- } else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
- infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
- if err != nil {
- return 0, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
- }
-
- envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
- secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
- recursive := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.Recursive
-
- plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath, recursive)
- if err != nil {
- return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
- }
-
- logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
-
- } else if authDetails.IsMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point
- plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.MachineIdentityScope)
-
- if err != nil {
- return 0, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
- }
-
- logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
-
- } else {
- return 0, errors.New("no authentication method provided. Please configure a authentication method then try again")
- }
-
- secretsCount = len(plainTextSecretsFromApi)
-
- if managedKubeSecret == nil {
- if err := r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, managedSecretReference, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
- return 0, fmt.Errorf("failed to create managed secret [err=%s]", err)
- }
- } else {
- if err := r.updateInfisicalManagedKubeSecret(ctx, logger, managedSecretReference, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
- return 0, fmt.Errorf("failed to update managed secret [err=%s]", err)
- }
}
}
diff --git a/k8-operator/packages/constants/constants.go b/k8-operator/packages/constants/constants.go
index 75f15606a..e5e2d8ff8 100644
--- a/k8-operator/packages/constants/constants.go
+++ b/k8-operator/packages/constants/constants.go
@@ -26,6 +26,13 @@ const (
PUSH_SECRET_DELETE_POLICY_ENABLED PushSecretDeletionPolicy = "Delete"
)
+type ManagedKubeResourceType string
+
+const (
+ MANAGED_KUBE_RESOURCE_TYPE_SECRET ManagedKubeResourceType = "Secret"
+ MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP ManagedKubeResourceType = "ConfigMap"
+)
+
type DynamicSecretLeaseRevocationPolicy string
const (
diff --git a/k8-operator/packages/model/model.go b/k8-operator/packages/model/model.go
index 2dbc6d259..3b3bc3216 100644
--- a/k8-operator/packages/model/model.go
+++ b/k8-operator/packages/model/model.go
@@ -11,11 +11,6 @@ type MachineIdentityDetails struct {
ClientSecret string
}
-type RequestUpdateUpdateDetails struct {
- Modified bool
- ETag string
-}
-
type SingleEnvironmentVariable struct {
Key string `json:"key"`
Value string `json:"value"`
diff --git a/k8-operator/packages/util/kubernetes.go b/k8-operator/packages/util/kubernetes.go
index 6397f4036..75d5d6bc4 100644
--- a/k8-operator/packages/util/kubernetes.go
+++ b/k8-operator/packages/util/kubernetes.go
@@ -25,6 +25,16 @@ func GetKubeSecretByNamespacedName(ctx context.Context, reconcilerClient client.
return kubeSecret, err
}
+func GetKubeConfigMapByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.ConfigMap, error) {
+ kubeConfigMap := &corev1.ConfigMap{}
+ err := reconcilerClient.Get(ctx, namespacedName, kubeConfigMap)
+ if err != nil {
+ kubeConfigMap = nil
+ }
+
+ return kubeConfigMap, err
+}
+
func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference) (machineIdentityDetails model.MachineIdentityDetails, err error) {
universalAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
diff --git a/k8-operator/packages/util/secrets.go b/k8-operator/packages/util/secrets.go
index b3325a701..22fa75271 100644
--- a/k8-operator/packages/util/secrets.go
+++ b/k8-operator/packages/util/secrets.go
@@ -6,7 +6,6 @@ import (
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
- "github.com/Infisical/infisical/k8-operator/packages/crypto"
"github.com/Infisical/infisical/k8-operator/packages/model"
"github.com/go-resty/resty/v2"
infisical "github.com/infisical/go-sdk"
@@ -49,7 +48,7 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil
}
-func GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalClientInterface, etag string, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
+func GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalClientInterface, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, error) {
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
ProjectSlug: secretScope.ProjectSlug,
@@ -61,7 +60,7 @@ func GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalCl
})
if err != nil {
- return nil, model.RequestUpdateUpdateDetails{}, err
+ return nil, fmt.Errorf("unable to get secrets. [err=%v]", err)
}
var environmentVariables []model.SingleEnvironmentVariable
@@ -77,18 +76,13 @@ func GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalCl
})
}
- newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", environmentVariables)))
-
- return environmentVariables, model.RequestUpdateUpdateDetails{
- Modified: etag != newEtag,
- ETag: newEtag,
- }, nil
+ return environmentVariables, nil
}
-func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClientInterface, fullServiceToken string, etag string, envSlug string, secretPath string, recursive bool) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
+func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClientInterface, fullServiceToken string, envSlug string, secretPath string, recursive bool) ([]model.SingleEnvironmentVariable, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
- return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
+ return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
@@ -100,7 +94,7 @@ func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClien
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil {
- return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
+ return nil, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
@@ -113,7 +107,7 @@ func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClien
})
if err != nil {
- return nil, model.RequestUpdateUpdateDetails{}, err
+ return nil, err
}
var environmentVariables []model.SingleEnvironmentVariable
@@ -129,31 +123,26 @@ func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClien
})
}
- newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", environmentVariables)))
-
- return environmentVariables, model.RequestUpdateUpdateDetails{
- Modified: etag != newEtag,
- ETag: newEtag,
- }, nil
+ return environmentVariables, nil
}
// Fetches plaintext secrets from an API endpoint using a service account.
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
-func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalClientInterface, serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
+func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalClientInterface, serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string) ([]model.SingleEnvironmentVariable, error) {
httpClient := resty.New()
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
SetHeader("Accept", "application/json")
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
if err != nil {
- return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
+ return nil, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
}
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
if err != nil {
- return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
+ return nil, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
}
// find key for requested project
@@ -165,7 +154,7 @@ func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalCli
}
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
- return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
+ return nil, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
}
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
@@ -178,7 +167,7 @@ func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalCli
})
if err != nil {
- return nil, model.RequestUpdateUpdateDetails{}, err
+ return nil, err
}
var environmentVariables []model.SingleEnvironmentVariable
@@ -193,49 +182,5 @@ func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalCli
})
}
- newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", environmentVariables)))
-
- return environmentVariables, model.RequestUpdateUpdateDetails{
- Modified: etag != newEtag,
- ETag: newEtag,
- }, nil
-}
-
-func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]model.SingleEnvironmentVariable {
- secretMapByName := make(map[string]model.SingleEnvironmentVariable, len(secrets))
-
- for _, secret := range secrets {
- secretMapByName[secret.Key] = secret
- }
-
- return secretMapByName
-}
-
-func MergeRawImportedSecrets(secrets []model.SingleEnvironmentVariable, importedSecrets []api.ImportedRawSecretV3) []model.SingleEnvironmentVariable {
- if importedSecrets == nil {
- return secrets
- }
-
- hasOverriden := make(map[string]bool)
- for _, sec := range secrets {
- hasOverriden[sec.Key] = true
- }
-
- for i := len(importedSecrets) - 1; i >= 0; i-- {
- importSec := importedSecrets[i]
-
- for _, sec := range importSec.Secrets {
- if _, ok := hasOverriden[sec.SecretKey]; !ok {
- secrets = append(secrets, model.SingleEnvironmentVariable{
- Key: sec.SecretKey,
- Value: sec.SecretValue,
- Type: sec.Type,
- ID: sec.ID,
- })
- hasOverriden[sec.SecretKey] = true
- }
- }
- }
-
- return secrets
+ return environmentVariables, nil
}
diff --git a/sink/oidc-server/main.js b/sink/oidc-server/main.js
new file mode 100644
index 000000000..0d5f10922
--- /dev/null
+++ b/sink/oidc-server/main.js
@@ -0,0 +1,87 @@
+import Provider from "oidc-provider";
+import express from "express";
+
+const configuration = {
+ jwks: {
+ keys: [
+ {
+ kty: "RSA",
+ use: "sig",
+ alg: "RS256",
+ d: "EF2Kky61jzvMYQ_B6ImXzCsQ8uQzbFJrGnB2azlpr_CFStjjUVKP4EKrSCVEasD6SGNJV2QSiNJr7j05nvuGmHMKa__rbU8fqP4qbDahUgCgWOq-zS5tGK6Ifk4II_cZ_V1F-TnrvmcOKMWBiSV-p8i72KpXXucbHGNRwASVs7--M55wp_m1UsybI2jSQ4IgyvGzTnvMmQ_GsX-XoD8u0zGU_4eN3DGc8l6hdxxuSymH0fEeL1Aj0LoCj6teRGF37a2sBQdU6mkNNAuyyirkoDqGZCGJToQLqX4F1FafnzjeIgfdneRa-vuaV380Hhr2rorWnQyBqOO27M5O_VAkJbfRaWJVrXTJ69ZgkU4GPdeYdklVL0HkU6laziTNqNMeAjnt4m51sWokVyJpvdWcb_vJ4NSCsRo7kHOz7g-UvWTXa8UW0DTDliq_TJ3rN4Gv0vn9tBlFfaeuLPpK4VNmRRDRXY_fcuzlnQwYExL9a4V_vCyGmabdb7PrUFPBcjR5",
+ dp: "SX52TkZEc_eLIk5gYrKjAC643LJIw1RxMBWWewRSGLn_rbrH1he3hy7AGDUV6Uon7zkNh9R5GBVuxmlluBRAGbrhIXAAf8sWeyma3F6FIAt-MH_VkfW5K2p88PLOyVGljlv8-Z3wzdKYOlDP4yFU18LqGMqaRSDLDGhILkuZhjLYA40sfYJeJTi_HVP5UyWL4ohayqUWCT2W3DgeDDThYHmufOaqlrSLhUst6uez_cDz0BXAYIZvUuPVL_n1-_px",
+ dq: "K1KYU77I6yyPA2u32rc0exp_TCG59hhpWxrmXN8yTXWyq_xYBhCJA_nHdY8UV25Hmd7q0iX2i8y2cCAFNWA5UWiSiNg9-fKRLI2nz53IM4dGfssOLwUk66wzX8r_u3XiLZsO7XNNtQZdcZmF0YuNTtzEdiNDhaOyHiwwHgShL36WNmUn00mZR__G5Qk60VvI8vsbvJU9xRnWuEVS1wRgyD7v6Nl9nIxb8N7oibCdTJLmgnRXPWvArsW0cJ-NURfr",
+ e: "AQAB",
+ n: "2QwX-NBMkQYedGpbPvHL7Ca0isvfmLC7lSc8XSOCLmCUIf6Bk_pdCNx2kxsmT81IoA8CfvJLHQj5vWKoVDFMLfwo4IujvsC3m2IrEg6jERE-YHfC3W5jKZtmzQYpfx5vC2_XTmcyPigtyaNVsftGfycES3B_tvphNsFmQcJjVGOsJQXXqh_TDv6FMcH4m9pngyw6wfe3GgAKA0dRTSfD0h7wLdNCeuid53lLpkQypTNdZ6_PiCMu2gr_cH5M0MPZtBb2TW12_2zOabExK1lI5-HvdPtbMT4Qzs2nd2NkjcWmlbKRZzq6IzyWt7W2EnfZDsi61PHECtTb-EQN2icl8Wnsp-0Bw66yviAOj0gn3X5hRLx-TknT_PnWMou17l5GoAojKDezcTW0iLlrfs2ixFlY28u7WklUN8uYhHvwgON6fsdefG-3bPpiRLBPZ_tgXa4doALsCwfXu2oz0vYktk31A-UYv92uJsKSUbK0_8ODTN0rslCqCYN_1a_aVt2P",
+ p: "--L5BX8juLlGJk8hdPgEUmJjD7SsZuMrdq3cSibkkbaWUE5CQQ7vhLPr2dWCS1jUnY9WyoCx9QCZvhTHjORX50ykkOyBso9VJjWvYPjsrPpF7_Y6V0dKlblDmbbmRT9BW-MgjbwTivu3c2OpMXh2XLF-FOTq3t3Brs7SRnhTkD6GBDFf3X95J0PF7NELa9z2-kzPSDYz3k-9FepXnRPBM_ViDzlRw4eKUdylVuhzGbC2TRSmab9BRP0wipQKd-f5",
+ q: "3Jd5CRJpQV3xUi3FiHHAwcjfsRkfXMrxfaXt0PjX2xWzxscYiDcyCF6VhHTAGsiq5SOtCp3l5mg6A9PzdR53AzM2-706D82fMwiUZvsLOVTepXkgriP_xw7rDlkOeAvjB80sL2G9scFliTzzRZ8I8E79A8DxZihfB75AIN9ijklEihnwxfhp2EgO5MYEyQRcqU1TT8wD8ekLMzd-kJUWyTz3BogiVJH__BQoB6kaDyjvQoxBgwh0hi72t9H5XqPH",
+ qi: "cwK0jhzwbu8BaTmTQhwfGiqwNN3v9F4nUQ4dtnBYRI6zlki4cLb2Mf9-VhyEsUYhhdTm8R7RwO9m5Xct3gEfozdk35wuvkVwkZgL3Uho5asao0xi4aENeUk5DCkU-paO3yLSDhIs9YYuYIDjUX6QuMCPjomypuE3SRm-Dg1PGOxYvX3w_P-0kd5iBFrm4jwGTZViFOr8tl_dXgDRDWDgofOYOYcmUv2_0zt1aO3j5dhEpwdkyuDMLfVZNpJQyopJ",
+ kid: "f262a3214213d194c92991d6735b153b",
+ },
+ ],
+ },
+ features: {
+ clientCredentials: {
+ enabled: true,
+ },
+ introspection: {
+ enabled: true,
+ },
+ resourceIndicators: {
+ enabled: true,
+ getResourceServerInfo(ctx, resourceIndicator) {
+ if (resourceIndicator === "urn:api") {
+ return {
+ scope: "read",
+ audience: "urn:api",
+ accessTokenTTL: 1 * 60 * 60, // 1 hour
+ accessTokenFormat: "jwt",
+ };
+ }
+
+ throw new errors.InvalidTarget();
+ },
+ },
+ },
+ clients: [
+ {
+ client_id: "app",
+ client_secret: "a_secret",
+ grant_types: ["client_credentials"],
+ redirect_uris: [],
+ response_types: [],
+ },
+ {
+ client_id: "oidc_client",
+ client_secret: "a_different_secret",
+ grant_types: ["authorization_code"],
+ response_types: ["code"],
+ redirect_uris: ["http://localhost:3001/cb"],
+ },
+ ],
+ claims: {
+ profile: [
+ "birthdate",
+ "family_name",
+ "gender",
+ "given_name",
+ "locale",
+ "middle_name",
+ "name",
+ "nickname",
+ "picture",
+ "preferred_username",
+ "profile",
+ "updated_at",
+ "website",
+ "zoneinfo",
+ ],
+ email: ["email", "email_verified"],
+ },
+};
+
+const oidc = new Provider("http://localhost:3000", configuration);
+
+const app = express();
+app.use("/oidc", oidc.callback());
+app.listen(3000);
diff --git a/sink/oidc-server/package-lock.json b/sink/oidc-server/package-lock.json
new file mode 100644
index 000000000..2be29633b
--- /dev/null
+++ b/sink/oidc-server/package-lock.json
@@ -0,0 +1,1708 @@
+{
+ "name": "oidc-server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "oidc-server",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "axios": "^1.8.3",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "form-data": "^4.0.2",
+ "jose": "^6.0.10",
+ "oidc-provider": "^8.8.1"
+ }
+ },
+ "node_modules/@koa/cors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz",
+ "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==",
+ "license": "MIT",
+ "dependencies": {
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@koa/router": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz",
+ "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==",
+ "license": "MIT",
+ "dependencies": {
+ "http-errors": "^2.0.0",
+ "koa-compose": "^4.1.0",
+ "path-to-regexp": "^6.3.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@koa/router/node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "license": "MIT"
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
+ "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+ "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
+ "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cache-content-type": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
+ "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^2.1.18",
+ "ylru": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
+ "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "10.2.14",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz",
+ "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "^4.0.2",
+ "get-stream": "^6.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "keyv": "^4.5.3",
+ "mimic-response": "^4.0.0",
+ "normalize-url": "^8.0.0",
+ "responselike": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cookies": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
+ "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "keygrip": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+ "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==",
+ "license": "MIT"
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT"
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.4.7",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/eta": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz",
+ "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/eta-dev/eta?sponsor=1"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
+ "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.17"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/got": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
+ "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^5.2.0",
+ "@szmarczak/http-timer": "^5.0.1",
+ "cacheable-lookup": "^7.0.0",
+ "cacheable-request": "^10.2.8",
+ "decompress-response": "^6.0.0",
+ "form-data-encoder": "^2.1.2",
+ "get-stream": "^6.0.1",
+ "http2-wrapper": "^2.1.10",
+ "lowercase-keys": "^3.0.0",
+ "p-cancelable": "^3.0.0",
+ "responselike": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-assert": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
+ "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==",
+ "license": "MIT",
+ "dependencies": {
+ "deep-equal": "~1.0.1",
+ "http-errors": "~1.8.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-assert/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/http-assert/node_modules/http-errors": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/http-assert/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http2-wrapper": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
+ "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/http2-wrapper/node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
+ "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.0",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz",
+ "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "license": "MIT"
+ },
+ "node_modules/keygrip": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
+ "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tsscmp": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/koa": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.0.tgz",
+ "integrity": "sha512-Afhqq0Vq3W7C+/rW6IqHVBDLzqObwZ07JaUNUEF8yCQ6afiyFE3RAy+i7V0E46XOWlH7vPWn/x0vsZwNy6PWxw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^1.3.5",
+ "cache-content-type": "^1.0.0",
+ "content-disposition": "~0.5.2",
+ "content-type": "^1.0.4",
+ "cookies": "~0.9.0",
+ "debug": "^4.3.2",
+ "delegates": "^1.0.0",
+ "depd": "^2.0.0",
+ "destroy": "^1.0.4",
+ "encodeurl": "^1.0.2",
+ "escape-html": "^1.0.3",
+ "fresh": "~0.5.2",
+ "http-assert": "^1.3.0",
+ "http-errors": "^1.6.3",
+ "is-generator-function": "^1.0.7",
+ "koa-compose": "^4.1.0",
+ "koa-convert": "^2.0.0",
+ "on-finished": "^2.3.0",
+ "only": "~0.0.2",
+ "parseurl": "^1.3.2",
+ "statuses": "^1.5.0",
+ "type-is": "^1.6.16",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
+ }
+ },
+ "node_modules/koa-compose": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz",
+ "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==",
+ "license": "MIT"
+ },
+ "node_modules/koa-convert": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz",
+ "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==",
+ "license": "MIT",
+ "dependencies": {
+ "co": "^4.6.0",
+ "koa-compose": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/koa/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/koa/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/koa/node_modules/http-errors": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/koa/node_modules/http-errors/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/koa/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/koa/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+ "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
+ "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
+ "integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz",
+ "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/oidc-provider": {
+ "version": "8.8.1",
+ "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.8.1.tgz",
+ "integrity": "sha512-qVChpayTwojUREJxLkFofUSK8kiSRIdzPrVSsoGibqRHl/YO60ege94OZS8vh7zaK+zxcG/Gu8UMaYB5ulohCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@koa/cors": "^5.0.0",
+ "@koa/router": "^13.1.0",
+ "debug": "^4.4.0",
+ "eta": "^3.5.0",
+ "got": "^13.0.0",
+ "jose": "^5.9.6",
+ "jsesc": "^3.1.0",
+ "koa": "^2.15.4",
+ "nanoid": "^5.0.9",
+ "object-hash": "^3.0.0",
+ "oidc-token-hash": "^5.0.3",
+ "quick-lru": "^7.0.0",
+ "raw-body": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/oidc-provider/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/oidc-provider/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/oidc-provider/node_modules/jose": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
+ "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/oidc-provider/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/oidc-provider/node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/oidc-token-hash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz",
+ "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/only": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
+ "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ=="
+ },
+ "node_modules/p-cancelable": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+ "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/quick-lru": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.0.0.tgz",
+ "integrity": "sha512-MX8gB7cVYTrYcFfAnfLlhRd0+Toyl8yX8uBx1MrX7K0jegiz9TumwOK27ldXrgDlHRdVi+MqU9Ssw6dr4BNreg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "license": "MIT"
+ },
+ "node_modules/responselike": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
+ "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tsscmp": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.x"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ylru": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz",
+ "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ }
+ }
+}
diff --git a/sink/oidc-server/package.json b/sink/oidc-server/package.json
new file mode 100644
index 000000000..514629d46
--- /dev/null
+++ b/sink/oidc-server/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "oidc-server",
+ "version": "1.0.0",
+ "main": "index.js",
+ "type": "module",
+ "scripts": {
+ "start": "node main.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "axios": "^1.8.3",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.2",
+ "form-data": "^4.0.2",
+ "jose": "^6.0.10",
+ "oidc-provider": "^8.8.1"
+ }
+}
diff --git a/sink/oidc-server/test-infisical.js b/sink/oidc-server/test-infisical.js
new file mode 100644
index 000000000..a2d8df022
--- /dev/null
+++ b/sink/oidc-server/test-infisical.js
@@ -0,0 +1,80 @@
+import axios from "axios";
+import { Buffer } from "buffer";
+import querystring from "querystring";
+
+// Configuration
+const config = {
+ issuer: "http://localhost:3000/oidc",
+ tokenEndpoint: "http://localhost:3000/oidc/token",
+ clientId: "app",
+ clientSecret: "a_secret",
+};
+
+// Client credentials flow for machine identity
+async function getMachineToken() {
+ try {
+ // Use application/x-www-form-urlencoded format as required by the OIDC spec
+ const data = querystring.stringify({
+ grant_type: "client_credentials",
+ scope: "read",
+ resource: "urn:api",
+ });
+
+ const authHeader =
+ "Basic " +
+ Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
+ "base64",
+ );
+
+ const response = await axios.post(config.tokenEndpoint, data, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Authorization: authHeader,
+ },
+ });
+
+ console.log("Successfully obtained token:");
+ console.log("Access Token:", response.data.access_token);
+ console.log("Token Type:", response.data.token_type);
+ console.log("Expires In:", response.data.expires_in, "seconds");
+ console.log("Scope:", response.data.scope);
+
+ return response.data;
+ } catch (error) {
+ console.error("Error obtaining token:");
+ if (error.response && error.response.data) {
+ console.error(error.response.data);
+ } else {
+ console.error(error.message);
+ }
+ throw error;
+ }
+}
+
+// Test the machine identity authentication
+async function testMachineIdentity() {
+ try {
+ // Get token using client credentials
+ const token = await getMachineToken();
+
+ const loginData = querystring.stringify({
+ identityId: "5d81d5cc-602f-4af7-b242-ab7c1331b430",
+ jwt: token.access_token,
+ });
+
+ const response = await axios({
+ method: "post",
+ url: `http://localhost:8080/api/v1/auth/oidc-auth/login`,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ data: loginData,
+ });
+ console.log(response.data);
+ } catch (error) {
+ console.error("Error in test:", error.message);
+ }
+}
+
+// Run the test
+testMachineIdentity();