diff --git a/backend/package-lock.json b/backend/package-lock.json index cd521119c..19f85472a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -61,6 +61,7 @@ "ldapjs": "^3.0.7", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", + "mongodb": "^6.8.1", "ms": "^2.1.3", "mysql2": "^3.9.8", "nanoid": "^3.3.4", @@ -4778,6 +4779,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", @@ -7214,6 +7223,19 @@ "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", "dev": true }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@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", @@ -8822,6 +8844,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", @@ -11174,15 +11204,46 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/gcp-metadata": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", - "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "peer": true, "dependencies": { - "gaxios": "^6.0.0", + "gaxios": "^5.0.0", "json-bigint": "^1.0.0" }, "engines": { - "node": ">=14" + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/generate-function": { @@ -11407,6 +11468,18 @@ "node": ">=14" } }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/google-auth-library/node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -13118,6 +13191,11 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -13308,6 +13386,91 @@ "obliterator": "^2.0.1" } }, + "node_modules/mongodb": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.1.tgz", + "integrity": "sha512-qsS+gl5EJb+VzJqUjXSZ5Y5rbuM/GZlZUEJ2OIVYP10L9rO9DQ0DGp+ceTzsmoADh6QYMWd9MSdG9IxRyYUkEA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -15927,6 +16090,14 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index 91f64c53d..f89843a00 100644 --- a/backend/package.json +++ b/backend/package.json @@ -158,6 +158,7 @@ "ldapjs": "^3.0.7", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", + "mongodb": "^6.8.1", "ms": "^2.1.3", "mysql2": "^3.9.8", "nanoid": "^3.3.4", diff --git a/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts b/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts new file mode 100644 index 000000000..fa359ab44 --- /dev/null +++ b/backend/src/db/migrations/20240909145938_cert-template-enforcement.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + const hasRequireTemplateForIssuanceColumn = await knex.schema.hasColumn( + TableName.CertificateAuthority, + "requireTemplateForIssuance" + ); + if (!hasRequireTemplateForIssuanceColumn) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false); + }); + } + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasTable(TableName.CertificateAuthority)) { + await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { + t.dropColumn("requireTemplateForIssuance"); + }); + } +} diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index e59a9225c..ffe0f7c44 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -28,7 +28,8 @@ export const CertificateAuthoritiesSchema = z.object({ keyAlgorithm: z.string(), notBefore: z.date().nullable().optional(), notAfter: z.date().nullable().optional(), - activeCaCertId: z.string().uuid().nullable().optional() + activeCaCertId: z.string().uuid().nullable().optional(), + requireTemplateForIssuance: z.boolean().default(false) }); export type TCertificateAuthorities = z.infer; diff --git a/backend/src/db/schemas/secret-sharing.ts b/backend/src/db/schemas/secret-sharing.ts index be5643e2b..2d0fc5eb5 100644 --- a/backend/src/db/schemas/secret-sharing.ts +++ b/backend/src/db/schemas/secret-sharing.ts @@ -21,8 +21,8 @@ export const SecretSharingSchema = z.object({ expiresAfterViews: z.number().nullable().optional(), accessType: z.string().default("anyone"), name: z.string().nullable().optional(), - password: z.string().nullable().optional(), - lastViewedAt: z.date().nullable().optional() + lastViewedAt: z.date().nullable().optional(), + password: z.string().nullable().optional() }); export type TSecretSharing = z.infer; diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 6001b8b6e..f0e69715d 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -118,7 +118,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { logger.error(error); - cb(null, {}); + cb(error as Error); } }, () => {} diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index 0e313b59b..804f56871 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -2,10 +2,11 @@ import { ForbiddenError } from "@casl/ability"; import { RawAxiosRequestHeaders } from "axios"; import { SecretKeyEncoding } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { validateLocalIps } from "@app/lib/validator"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue"; import { TLicenseServiceFactory } from "../license/license-service"; @@ -44,6 +45,7 @@ export const auditLogStreamServiceFactory = ({ }: TCreateAuditLogStreamDTO) => { if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" }); + const appCfg = getConfig(); const plan = await licenseService.getPlan(actorOrgId); if (!plan.auditLogStreams) throw new BadRequestError({ @@ -59,7 +61,9 @@ export const auditLogStreamServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); - validateLocalIps(url); + if (appCfg.isCloud) { + blockLocalAndPrivateIpAddresses(url); + } const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId }); if (totalStreams.length >= plan.auditLogStreamLimit) { @@ -131,7 +135,8 @@ export const auditLogStreamServiceFactory = ({ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); - if (url) validateLocalIps(url); + const appCfg = getConfig(); + if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url); // testing connection first const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; 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 c9d04602d..da5298b64 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -140,6 +140,7 @@ export enum EventType { GET_CA_CRLS = "get-certificate-authority-crls", ISSUE_CERT = "issue-cert", SIGN_CERT = "sign-cert", + GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates", GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", @@ -1192,6 +1193,14 @@ interface SignCert { }; } +interface GetCaCertificateTemplates { + type: EventType.GET_CA_CERTIFICATE_TEMPLATES; + metadata: { + caId: string; + dn: string; + }; +} + interface GetCert { type: EventType.GET_CERT; metadata: { @@ -1547,6 +1556,7 @@ export type Event = | GetCaCrls | IssueCert | SignCert + | GetCaCertificateTemplates | GetCert | DeleteCert | RevokeCert diff --git a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts index 2ead58bef..3247ef21b 100644 --- a/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts +++ b/backend/src/ee/services/dynamic-secret/providers/elastic-search.ts @@ -17,7 +17,7 @@ const generateUsername = () => { return alphaNumericNanoId(32); }; -export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => { +export const ElasticSearchProvider = (): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const appCfg = getConfig(); const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 23769df32..6ae22c869 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,9 +1,11 @@ import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; import { CassandraProvider } from "./cassandra"; -import { ElasticSearchDatabaseProvider } from "./elastic-search"; +import { ElasticSearchProvider } from "./elastic-search"; import { DynamicSecretProviders } from "./models"; import { MongoAtlasProvider } from "./mongo-atlas"; +import { MongoDBProvider } from "./mongo-db"; +import { RabbitMqProvider } from "./rabbit-mq"; import { RedisDatabaseProvider } from "./redis"; import { SqlDatabaseProvider } from "./sql-database"; @@ -14,5 +16,7 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.Redis]: RedisDatabaseProvider(), [DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(), [DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(), - [DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider() + [DynamicSecretProviders.MongoDB]: MongoDBProvider(), + [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), + [DynamicSecretProviders.RabbitMq]: RabbitMqProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 06069c981..f23a60df7 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -17,7 +17,6 @@ export const DynamicSecretRedisDBSchema = z.object({ port: z.number(), username: z.string().trim(), // this is often "default". password: z.string().trim().optional(), - creationStatement: z.string().trim(), revocationStatement: z.string().trim(), renewStatement: z.string().trim().optional(), @@ -57,6 +56,26 @@ export const DynamicSecretElasticSearchSchema = z.object({ ca: z.string().optional() }); +export const DynamicSecretRabbitMqSchema = z.object({ + host: z.string().trim().min(1), + port: z.number(), + tags: z.array(z.string().trim()).default([]), + + username: z.string().trim().min(1), + password: z.string().trim().min(1), + + ca: z.string().optional(), + + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }) +}); + export const DynamicSecretSqlDBSchema = z.object({ client: z.nativeEnum(SqlProviders), host: z.string().trim().toLowerCase(), @@ -131,6 +150,22 @@ export const DynamicSecretMongoAtlasSchema = z.object({ .array() }); +export const DynamicSecretMongoDBSchema = z.object({ + host: z.string().min(1).trim().toLowerCase(), + port: z.number().optional(), + username: z.string().min(1).trim(), + password: z.string().min(1).trim(), + database: z.string().min(1).trim(), + ca: z.string().min(1).optional(), + roles: z + .string() + .array() + .min(1) + .describe( + 'Enum: "atlasAdmin" "backup" "clusterMonitor" "dbAdmin" "dbAdminAnyDatabase" "enableSharding" "read" "readAnyDatabase" "readWrite" "readWriteAnyDatabase" "".Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.' + ) +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -138,7 +173,9 @@ export enum DynamicSecretProviders { Redis = "redis", AwsElastiCache = "aws-elasticache", MongoAtlas = "mongo-db-atlas", - ElasticSearch = "elastic-search" + ElasticSearch = "elastic-search", + MongoDB = "mongo-db", + RabbitMq = "rabbit-mq" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -148,7 +185,9 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }), - z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }) + z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), + z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), + z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts new file mode 100644 index 000000000..ea9430846 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/mongo-db.ts @@ -0,0 +1,116 @@ +import { MongoClient } from "mongodb"; +import { customAlphabet } from "nanoid"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = (size = 48) => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 48)(size); +}; + +const generateUsername = () => { + return alphaNumericNanoId(32); +}; + +export const MongoDBProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const appCfg = getConfig(); + const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs); + if ( + appCfg.isCloud && + // localhost + // internal ips + (providerInputs.host === "host.docker.internal" || + providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) || + providerInputs.host.match(/^192\.168\.\d+\.\d+/)) + ) + throw new BadRequestError({ message: "Invalid db host" }); + + if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") { + throw new BadRequestError({ message: "Invalid db host" }); + } + + return providerInputs; + }; + + const getClient = async (providerInputs: z.infer) => { + const isSrv = !providerInputs.port; + const uri = isSrv + ? `mongodb+srv://${providerInputs.host}` + : `mongodb://${providerInputs.host}:${providerInputs.port}`; + + const client = new MongoClient(uri, { + auth: { + username: providerInputs.username, + password: providerInputs.password + }, + directConnection: !isSrv, + ca: providerInputs.ca + }); + return client; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const client = await getClient(providerInputs); + + const isConnected = await client + .db(providerInputs.database) + .command({ ping: 1 }) + .then(() => true); + + await client.close(); + return isConnected; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const client = await getClient(providerInputs); + + const username = generateUsername(); + const password = generatePassword(); + + const db = client.db(providerInputs.database); + + await db.command({ + createUser: username, + pwd: password, + roles: providerInputs.roles + }); + await client.close(); + + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + const providerInputs = await validateProviderInputs(inputs); + const client = await getClient(providerInputs); + + const username = entityId; + + const db = client.db(providerInputs.database); + await db.command({ + dropUser: username + }); + await client.close(); + + return { entityId: username }; + }; + + const renew = async (_inputs: unknown, entityId: string) => { + return { entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts new file mode 100644 index 000000000..15492cd2d --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/rabbit-mq.ts @@ -0,0 +1,172 @@ +import axios, { Axios } from "axios"; +import https from "https"; +import { customAlphabet } from "nanoid"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +const generateUsername = () => { + return alphaNumericNanoId(32); +}; + +type TCreateRabbitMQUser = { + axiosInstance: Axios; + createUser: { + username: string; + password: string; + tags: string[]; + }; + virtualHost: { + name: string; + permissions: { + read: string; + write: string; + configure: string; + }; + }; +}; + +type TDeleteRabbitMqUser = { + axiosInstance: Axios; + usernameToDelete: string; +}; + +async function createRabbitMqUser({ axiosInstance, createUser, virtualHost }: TCreateRabbitMQUser): Promise { + try { + // Create user + const userUrl = `/users/${createUser.username}`; + const userData = { + password: createUser.password, + tags: createUser.tags.join(",") + }; + + await axiosInstance.put(userUrl, userData); + + // Set permissions for the virtual host + if (virtualHost) { + const permissionData = { + configure: virtualHost.permissions.configure, + write: virtualHost.permissions.write, + read: virtualHost.permissions.read + }; + + await axiosInstance.put( + `/permissions/${encodeURIComponent(virtualHost.name)}/${createUser.username}`, + permissionData + ); + } + } catch (error) { + logger.error(error, "Error creating RabbitMQ user"); + throw error; + } +} + +async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRabbitMqUser) { + await axiosInstance.delete(`users/${usernameToDelete}`); + return { username: usernameToDelete }; +} + +export const RabbitMqProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const appCfg = getConfig(); + const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not + + const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs); + if ( + isCloud && + // localhost + // internal ips + (providerInputs.host === "host.docker.internal" || + providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) || + providerInputs.host.match(/^192\.168\.\d+\.\d+/)) + ) { + throw new BadRequestError({ message: "Invalid db host" }); + } + if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") { + throw new BadRequestError({ message: "Invalid db host" }); + } + + return providerInputs; + }; + + const getClient = async (providerInputs: z.infer) => { + const axiosInstance = axios.create({ + baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`, + auth: { + username: providerInputs.username, + password: providerInputs.password + }, + headers: { + "Content-Type": "application/json" + }, + + ...(providerInputs.ca && { + httpsAgent: new https.Agent({ ca: providerInputs.ca, rejectUnauthorized: false }) + }) + }); + + return axiosInstance; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const infoResponse = await connection.get("/whoami").then(() => true); + + return infoResponse; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = generateUsername(); + const password = generatePassword(); + + await createRabbitMqUser({ + axiosInstance: connection, + virtualHost: providerInputs.virtualHost, + createUser: { + password, + username, + tags: [...(providerInputs.tags ?? []), "infisical-user"] + } + }); + + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId }); + + return { entityId }; + }; + + const renew = async (inputs: unknown, entityId: string) => { + // Do nothing + return { entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index d38620837..b08c70d93 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1037,14 +1037,18 @@ export const CERTIFICATE_AUTHORITIES = { maxPathLength: "The maximum number of intermediate CAs that may follow this CA in the certificate / CA chain. A maxPathLength of -1 implies no path limit on the chain.", keyAlgorithm: - "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA." + "The type of public key algorithm and size, in bits, of the key pair for the CA; when you create an intermediate CA, you must use a key algorithm supported by the parent CA.", + requireTemplateForIssuance: + "Whether or not certificates for this CA can only be issued through certificate templates." }, GET: { caId: "The ID of the CA to get" }, UPDATE: { caId: "The ID of the CA to update", - status: "The status of the CA to update to. This can be one of active or disabled" + status: "The status of the CA to update to. This can be one of active or disabled", + requireTemplateForIssuance: + "Whether or not certificates for this CA can only be issued through certificate templates." }, DELETE: { caId: "The ID of the CA to delete" diff --git a/backend/src/lib/validator/index.ts b/backend/src/lib/validator/index.ts index 6a70d8571..4340d0210 100644 --- a/backend/src/lib/validator/index.ts +++ b/backend/src/lib/validator/index.ts @@ -1,2 +1,2 @@ export { isDisposableEmail } from "./validate-email"; -export { validateLocalIps } from "./validate-url"; +export { blockLocalAndPrivateIpAddresses } from "./validate-url"; diff --git a/backend/src/lib/validator/validate-url.ts b/backend/src/lib/validator/validate-url.ts index 9a953be1a..fccebf47b 100644 --- a/backend/src/lib/validator/validate-url.ts +++ b/backend/src/lib/validator/validate-url.ts @@ -1,7 +1,7 @@ import { getConfig } from "../config/env"; import { BadRequestError } from "../errors"; -export const validateLocalIps = (url: string) => { +export const blockLocalAndPrivateIpAddresses = (url: string) => { const validUrl = new URL(url); const appCfg = getConfig(); // on cloud local ips are not allowed diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 1ab8bc3fd..726f8431c 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -10,7 +10,7 @@ import fastifyFormBody from "@fastify/formbody"; import helmet from "@fastify/helmet"; import type { FastifyRateLimitOptions } from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit"; -import fasitfy from "fastify"; +import fastify from "fastify"; import { Knex } from "knex"; import { Logger } from "pino"; @@ -39,7 +39,7 @@ type TMain = { // Run the server! export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { const appCfg = getConfig(); - const server = fasitfy({ + const server = fastify({ logger: appCfg.NODE_ENV === "test" ? false : logger, trustProxy: true, connectionTimeout: 30 * 1000, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 70e7639b2..dcac9366f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -470,7 +470,7 @@ export const registerRoutes = async ( projectMembershipDAL }); - const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL }); + const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL }); const passwordService = authPaswordServiceFactory({ tokenService, smtpService, diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 9a866f66e..77ee70e57 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,7 +1,7 @@ import ms from "ms"; import { z } from "zod"; -import { CertificateAuthoritiesSchema } from "@app/db/schemas"; +import { CertificateAuthoritiesSchema, CertificateTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -42,7 +42,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { keyAlgorithm: z .nativeEnum(CertKeyAlgorithm) .default(CertKeyAlgorithm.RSA_2048) - .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm) + .describe(CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm), + requireTemplateForIssuance: z + .boolean() + .default(false) + .describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance) }) .refine( (data) => { @@ -148,7 +152,11 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.UPDATE.caId) }), body: z.object({ - status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status) + status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]).optional().describe(CERTIFICATE_AUTHORITIES.UPDATE.status), + requireTemplateForIssuance: z + .boolean() + .optional() + .describe(CERTIFICATE_AUTHORITIES.CREATE.requireTemplateForIssuance) }), response: { 200: z.object({ @@ -700,6 +708,51 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:caId/certificate-templates", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get list of certificate templates for the CA", + params: z.object({ + caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId) + }), + response: { + 200: z.object({ + certificateTemplates: CertificateTemplatesSchema.array() + }) + } + }, + handler: async (req) => { + const { certificateTemplates, ca } = await server.services.certificateAuthority.getCaCertificateTemplates({ + caId: req.params.caId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: ca.projectId, + event: { + type: EventType.GET_CA_CERTIFICATE_TEMPLATES, + metadata: { + caId: ca.id, + dn: ca.dn + } + } + }); + + return { + certificateTemplates + }; + } + }); + server.route({ method: "GET", url: "/:caId/crls", diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index 3d8d02e6f..cea67999c 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -57,7 +57,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { logger.error(error); - cb(null, false); + cb(error as Error, false); } } ) @@ -91,7 +91,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { logger.error(error); - cb(null, false); + cb(error as Error, false); } } ) @@ -126,7 +126,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { logger.error(error); - cb(null, false); + cb(error as Error, false); } } ) diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 61a0c74e5..b5f523a54 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -42,7 +42,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - organizationId: z.string().trim() + organizationId: z.string().trim(), + userAgent: z.enum(["cli"]).optional() }), response: { 200: z.object({ @@ -53,7 +54,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { handler: async (req, res) => { const cfg = getConfig(); const tokens = await server.services.login.selectOrganization({ - userAgent: req.headers["user-agent"], + userAgent: req.body.userAgent ?? req.headers["user-agent"], authJwtToken: req.headers.authorization, organizationId: req.body.organizationId, ipAddress: req.realIp diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 91bf40198..fb38c024e 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -12,7 +12,6 @@ import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/erro import { logger } from "@app/lib/logger"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; -import { TTokenDALFactory } from "../auth-token/auth-token-dal"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TOrgDALFactory } from "../org/org-dal"; @@ -34,7 +33,6 @@ type TAuthLoginServiceFactoryDep = { orgDAL: TOrgDALFactory; tokenService: TAuthTokenServiceFactory; smtpService: TSmtpService; - tokenDAL: TTokenDALFactory; }; export type TAuthLoginFactory = ReturnType; @@ -42,8 +40,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService, - orgDAL, - tokenDAL + orgDAL }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -376,8 +373,6 @@ export const authLoginServiceFactory = ({ }); } - await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId); - const tokens = await generateUserTokens({ authMethod: decodedToken.authMethod, user, diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index a1f062a45..1c2a5a689 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -41,6 +41,7 @@ import { TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, + TGetCaCertificateTemplatesDTO, TGetCaCertsDTO, TGetCaCsrDTO, TGetCaDTO, @@ -64,7 +65,7 @@ type TCertificateAuthorityServiceFactoryDep = { >; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; - certificateTemplateDAL: Pick; + certificateTemplateDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; certificateBodyDAL: Pick; @@ -108,6 +109,7 @@ export const certificateAuthorityServiceFactory = ({ notAfter, maxPathLength, keyAlgorithm, + requireTemplateForIssuance, actorId, actorAuthMethod, actor, @@ -170,7 +172,8 @@ export const certificateAuthorityServiceFactory = ({ notBefore: notBeforeDate, notAfter: notAfterDate, serialNumber - }) + }), + requireTemplateForIssuance }, tx ); @@ -213,7 +216,6 @@ export const certificateAuthorityServiceFactory = ({ keys, extensions: [ new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true), - new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true), // eslint-disable-next-line no-bitwise new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), await x509.SubjectKeyIdentifierExtension.create(keys.publicKey) @@ -303,7 +305,15 @@ export const certificateAuthorityServiceFactory = ({ * Update CA with id [caId]. * Note: Used to enable/disable CA */ - const updateCaById = async ({ caId, status, actorId, actorAuthMethod, actor, actorOrgId }: TUpdateCaDTO) => { + const updateCaById = async ({ + caId, + status, + requireTemplateForIssuance, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findById(caId); if (!ca) throw new BadRequestError({ message: "CA not found" }); @@ -320,7 +330,7 @@ export const certificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - const updatedCa = await certificateAuthorityDAL.updateById(caId, { status }); + const updatedCa = await certificateAuthorityDAL.updateById(caId, { status, requireTemplateForIssuance }); return updatedCa; }; @@ -496,7 +506,6 @@ export const certificateAuthorityServiceFactory = ({ ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength, true ), - new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true), // eslint-disable-next-line no-bitwise new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), await x509.SubjectKeyIdentifierExtension.create(caPublicKey) @@ -1079,6 +1088,9 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance && !certificateTemplate) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); if (ca.notAfter && new Date() > new Date(ca.notAfter)) { @@ -1349,6 +1361,9 @@ export const certificateAuthorityServiceFactory = ({ if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance && !certificateTemplate) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); @@ -1570,6 +1585,40 @@ export const certificateAuthorityServiceFactory = ({ }; }; + /** + * Return list of certificate templates for CA with id [caId]. + */ + const getCaCertificateTemplates = async ({ + caId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetCaCertificateTemplatesDTO) => { + const ca = await certificateAuthorityDAL.findById(caId); + if (!ca) throw new BadRequestError({ message: "CA not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + ca.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + + const certificateTemplates = await certificateTemplateDAL.find({ caId }); + + return { + certificateTemplates, + ca + }; + }; + return { createCa, getCaById, @@ -1582,6 +1631,7 @@ export const certificateAuthorityServiceFactory = ({ signIntermediate, importCertToCa, issueCertFromCa, - signCertFromCa + signCertFromCa, + getCaCertificateTemplates }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index 764a5dca9..5876c0057 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -38,6 +38,7 @@ export type TCreateCaDTO = { notAfter?: string; maxPathLength: number; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; } & Omit; export type TGetCaDTO = { @@ -47,6 +48,7 @@ export type TGetCaDTO = { export type TUpdateCaDTO = { caId: string; status?: CaStatus; + requireTemplateForIssuance?: boolean; } & Omit; export type TDeleteCaDTO = { @@ -125,6 +127,10 @@ export type TSignCertFromCaDTO = notAfter?: string; } & Omit); +export type TGetCaCertificateTemplatesDTO = { + caId: string; +} & Omit; + export type TDNParts = { commonName?: string; organization?: string; diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index a6d6c8c23..16e7dcf49 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -7,7 +7,7 @@ const isValidDate = (dateString: string) => { export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" }); -export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/; +export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/; export const validateAltNamesField = z .string() .trim() diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts new file mode 100644 index 000000000..c6d65d836 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts @@ -0,0 +1,4 @@ +import picomatch from "picomatch"; + +export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) => + policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); 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 457e93058..4c687e86c 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 @@ -28,6 +28,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TOrgBotDALFactory } from "../org/org-bot-dal"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; +import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { TAttachOidcAuthDTO, TGetOidcAuthDTO, @@ -123,7 +124,7 @@ export const identityOidcAuthServiceFactory = ({ }) as Record; if (identityOidcAuth.boundSubject) { - if (tokenData.sub !== identityOidcAuth.boundSubject) { + if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) { throw new ForbiddenRequestError({ message: "Access denied: OIDC subject not allowed." }); @@ -131,7 +132,11 @@ export const identityOidcAuthServiceFactory = ({ } if (identityOidcAuth.boundAudiences) { - if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) { + if ( + !identityOidcAuth.boundAudiences + .split(", ") + .some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue)) + ) { throw new ForbiddenRequestError({ message: "Access denied: OIDC audience not allowed." }); @@ -142,7 +147,9 @@ export const identityOidcAuthServiceFactory = ({ Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => { const claimValue = (identityOidcAuth.boundClaims as Record)[claimKey]; // handle both single and multi-valued claims - if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) { + if ( + !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry)) + ) { throw new ForbiddenRequestError({ message: "Access denied: OIDC claim not allowed." }); diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index d0fa2279a..97525b032 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -567,8 +567,8 @@ const syncSecretsAWSParameterStore = async ({ }); ssm.config.update(config); - const metadata = z.record(z.any()).parse(integration.metadata || {}); - const awsParameterStoreSecretsObj: Record = {}; + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + const awsParameterStoreSecretsObj: Record = {}; logger.info( `getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]` ); @@ -598,18 +598,57 @@ const syncSecretsAWSParameterStore = async ({ nextToken = parameters.NextToken; } - logger.info( - `getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${ - integration.environment.slug - }] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys( - awsParameterStoreSecretsObj - ).join(",")}]` - ); - logger.info( - `getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${ - integration.environment.slug - }] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]` - ); + let areParametersKmsKeysFetched = false; + + if (metadata.kmsKeyId) { + // we put this inside a try catch so that existing integrations without the ssm:DescribeParameters + // AWS permission will not break + try { + let hasNextDescribePage = true; + let describeNextToken: string | undefined; + + while (hasNextDescribePage) { + const parameters = await ssm + .describeParameters({ + MaxResults: 10, + NextToken: describeNextToken, + ParameterFilters: [ + { + Key: "Path", + Option: "OneLevel", + Values: [integration.path as string] + } + ] + }) + .promise(); + + if (parameters.Parameters) { + parameters.Parameters.forEach((parameter) => { + if (parameter.Name) { + const secKey = parameter.Name.substring((integration.path as string).length); + awsParameterStoreSecretsObj[secKey].KeyId = parameter.KeyId; + } + }); + } + areParametersKmsKeysFetched = true; + hasNextDescribePage = Boolean(parameters.NextToken); + describeNextToken = parameters.NextToken; + } + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((error as any).code === "AccessDeniedException") { + logger.error( + `AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)` + ); + } + + response = { + isSynced: false, + syncMessage: (error as AWSError)?.message || "Error syncing with AWS Parameter Store" + }; + } + } + // Identify secrets to create // don't use Promise.all() and promise map here // it will cause rate limit @@ -620,7 +659,7 @@ const syncSecretsAWSParameterStore = async ({ // -> create secret if (secrets[key].value) { logger.info( - `getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + `getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]` ); await ssm .putParameter({ @@ -648,7 +687,7 @@ const syncSecretsAWSParameterStore = async ({ } catch (err) { logger.error( err, - `getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + `getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]` ); // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((err as any).code === "AccessDeniedException") { @@ -667,16 +706,23 @@ const syncSecretsAWSParameterStore = async ({ // case: secret exists in AWS parameter store } else { logger.info( - `getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + `getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]` ); - // -> update secret - if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { + + const shouldUpdateKms = + areParametersKmsKeysFetched && + Boolean(metadata.kmsKeyId) && + awsParameterStoreSecretsObj[key].KeyId !== metadata.kmsKeyId; + + // we ensure that the KMS key configured in the integration is applied for ALL parameters on AWS + if (shouldUpdateKms || awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { await ssm .putParameter({ Name: `${integration.path}${key}`, Type: "SecureString", Value: secrets[key].value, - Overwrite: true + Overwrite: true, + ...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }) }) .promise(); } @@ -698,7 +744,7 @@ const syncSecretsAWSParameterStore = async ({ } catch (err) { logger.error( err, - `getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]` + `getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}]` ); // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((err as any).code === "AccessDeniedException") { @@ -728,11 +774,11 @@ const syncSecretsAWSParameterStore = async ({ for (const key in awsParameterStoreSecretsObj) { if (Object.hasOwn(awsParameterStoreSecretsObj, key)) { logger.info( - `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]` + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=2]` ); if (!(key in secrets)) { logger.info( - `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]` + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=3]` ); // case: // -> delete secret @@ -742,7 +788,7 @@ const syncSecretsAWSParameterStore = async ({ }) .promise(); logger.info( - `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]` + `getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=4]` ); } await new Promise((resolve) => { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 7079c17ef..65cd3a0e4 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -444,7 +444,9 @@ export const expandSecretReferencesFactory = ({ depth: depth + 1 }); } - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + if (referedValue) { + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + } } else { const secretReferenceEnvironment = entities[0]; const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1)); @@ -463,7 +465,9 @@ export const expandSecretReferencesFactory = ({ }); } - expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + if (referedValue) { + expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue); + } } } } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 22a4ca65b..fa5176d89 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -4,22 +4,27 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( + "errors" "fmt" "os" "os/exec" "os/signal" "runtime" "strings" + "sync" "syscall" + "time" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" - "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) +var ErrManualSignalInterrupt = errors.New("signal: interrupt") +var watcherWaitGroup = new(sync.WaitGroup) + // runCmd represents the run command var runCmd = &cobra.Command{ Example: ` @@ -77,11 +82,35 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + command, err := cmd.Flags().GetString("command") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + secretOverriding, err := cmd.Flags().GetBool("secret-overriding") if err != nil { util.HandleError(err, "Unable to parse flag") } + watchMode, err := cmd.Flags().GetBool("watch") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + watchModeInterval, err := cmd.Flags().GetInt("watch-interval") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + // If the --watch flag has been set, the --watch-interval flag should also be set + if watchMode && watchModeInterval < 5 { + util.HandleError(fmt.Errorf("watch interval must be at least 5 seconds, you passed %d seconds", watchModeInterval)) + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -116,108 +145,50 @@ var runCmd = &cobra.Command{ Recursive: recursive, } - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - request.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - request.UniversalAuthAccessToken = token.Token - } - - secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir) - + injectableEnvironment, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, shouldExpandSecrets, token) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") } - if secretOverriding { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) + log.Debug().Msgf("injecting the following environment variables into shell: %v", injectableEnvironment.Variables) + + if watchMode { + executeCommandWithWatchMode(command, args, watchModeInterval, request, projectConfigDir, shouldExpandSecrets, secretOverriding, token) } else { - secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) - } + if cmd.Flags().Changed("command") { + command := cmd.Flag("command").Value.String() + err = executeMultipleCommandWithEnvs(command, injectableEnvironment.SecretsCount, injectableEnvironment.Variables) + if err != nil { + fmt.Println(err) + os.Exit(1) + } - if shouldExpandSecrets { - - authParams := models.ExpandSecretsAuthentication{} - - if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { - authParams.InfisicalToken = token.Token - } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { - authParams.UniversalAuthAccessToken = token.Token - } - - secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir) - } - - secretsByKey := getSecretsByKeys(secrets) - environmentVariables := make(map[string]string) - - // add all existing environment vars - for _, s := range os.Environ() { - kv := strings.SplitN(s, "=", 2) - key := kv[0] - value := kv[1] - environmentVariables[key] = value - } - - // check to see if there are any reserved key words in secrets to inject - filterReservedEnvVars(secretsByKey) - - // now add infisical secrets - for k, v := range secretsByKey { - environmentVariables[k] = v.Value - } - - // turn it back into a list of envs - var env []string - for key, value := range environmentVariables { - s := key + "=" + value - env = append(env, s) - } - - log.Debug().Msgf("injecting the following environment variables into shell: %v", env) - - Telemetry.CaptureEvent("cli-command:run", - posthog.NewProperties(). - Set("secretsCount", len(secrets)). - Set("environment", environmentName). - Set("isUsingServiceToken", token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER). - Set("isUsingUniversalAuthToken", token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER). - Set("single-command", strings.Join(args, " ")). - Set("multi-command", cmd.Flag("command").Value.String()). - Set("version", util.CLI_VERSION)) - - if cmd.Flags().Changed("command") { - command := cmd.Flag("command").Value.String() - - err = executeMultipleCommandWithEnvs(command, len(secretsByKey), env) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - } else { - err = executeSingleCommandWithEnvs(args, len(secretsByKey), env) - if err != nil { - fmt.Println(err) - os.Exit(1) + } else { + err = executeSingleCommandWithEnvs(args, injectableEnvironment.SecretsCount, injectableEnvironment.Variables) + if err != nil { + fmt.Println(err) + os.Exit(1) + } } } + }, } -var ( - reservedEnvVars = []string{ - "HOME", "PATH", "PS1", "PS2", - "PWD", "EDITOR", "XAUTHORITY", "USER", - "TERM", "TERMINFO", "SHELL", "MAIL", - } - - reservedEnvVarPrefixes = []string{ - "XDG_", - "LC_", - } -) - func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { + var ( + reservedEnvVars = []string{ + "HOME", "PATH", "PS1", "PS2", + "PWD", "EDITOR", "XAUTHORITY", "USER", + "TERM", "TERMINFO", "SHELL", "MAIL", + } + + reservedEnvVarPrefixes = []string{ + "XDG_", + "LC_", + } + ) + for _, reservedEnvName := range reservedEnvVars { if _, ok := env[reservedEnvName]; ok { delete(env, reservedEnvName) @@ -237,13 +208,15 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { func init() { rootCmd.AddCommand(runCmd) - runCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + runCmd.Flags().String("token", "", "fetch secrets using service token or machine identity access token") runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") - runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") - runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") - runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") - runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") - runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") + runCmd.Flags().StringP("env", "e", "dev", "set the environment (dev, prod, etc.) from which your secrets should be pulled from") + runCmd.Flags().Bool("expand", true, "parse shell parameter expansions in your secrets") + runCmd.Flags().Bool("include-imports", true, "import linked secrets ") + runCmd.Flags().Bool("recursive", false, "fetch secrets from all sub-folders") + runCmd.Flags().Bool("secret-overriding", true, "prioritizes personal secrets, if any, with the same name over shared secrets") + runCmd.Flags().Bool("watch", false, "enable reload of application when secrets change") + runCmd.Flags().Int("watch-interval", 10, "interval in seconds to check for secret changes") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") runCmd.Flags().String("path", "/", "get secrets within a folder path") @@ -263,7 +236,7 @@ func executeSingleCommandWithEnvs(args []string, secretsCount int, env []string) cmd.Stderr = os.Stderr cmd.Env = env - return execCmd(cmd) + return execBasicCmd(cmd) } func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []string) error { @@ -286,11 +259,10 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env [] log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", secretsCount)) log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand) - return execCmd(cmd) + return execBasicCmd(cmd) } -// Credit: inspired by AWS Valut -func execCmd(cmd *exec.Cmd) error { +func execBasicCmd(cmd *exec.Cmd) error { sigChannel := make(chan os.Signal, 1) signal.Notify(sigChannel) @@ -314,3 +286,217 @@ func execCmd(cmd *exec.Cmd) error { os.Exit(waitStatus.ExitStatus()) return nil } + +func waitForExitCommand(cmd *exec.Cmd) (int, error) { + if err := cmd.Wait(); err != nil { + // ignore errors + cmd.Process.Signal(os.Kill) // #nosec G104 + + if exitError, ok := err.(*exec.ExitError); ok { + return exitError.ExitCode(), exitError + } + + return 2, err + } + + waitStatus, ok := cmd.ProcessState.Sys().(syscall.WaitStatus) + if !ok { + return 2, fmt.Errorf("unexpected ProcessState type, expected syscall.WaitStatus, got %T", waitStatus) + } + return waitStatus.ExitStatus(), nil +} + +func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInterval int, request models.GetAllSecretsParameters, projectConfigDir string, expandSecrets bool, secretOverriding bool, token *models.TokenDetails) { + + var cmd *exec.Cmd + var err error + var lastSecretsFetch time.Time + var lastUpdateEvent time.Time + var watchMutex sync.Mutex + var processMutex sync.Mutex + var beingTerminated = false + var currentETag string + + if err != nil { + util.HandleError(err, "Failed to fetch secrets") + } + + runCommandWithWatcher := func(environmentVariables models.InjectableEnvironmentResult) { + currentETag = environmentVariables.ETag + secretsFetchedAt := time.Now() + if secretsFetchedAt.After(lastSecretsFetch) { + lastSecretsFetch = secretsFetchedAt + } + + shouldRestartProcess := cmd != nil + // terminate the old process before starting a new one + if shouldRestartProcess { + log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Environment changes detected. Reloading process...")) + beingTerminated = true + + log.Debug().Msgf(color.HiMagentaString("[HOT RELOAD] Sending SIGTERM to PID %d", cmd.Process.Pid)) + if e := cmd.Process.Signal(syscall.SIGTERM); e != nil { + log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGTERM")) + } + // wait up to 10 sec for the process to exit + for i := 0; i < 10; i++ { + if !util.IsProcessRunning(cmd.Process) { + // process has been killed so we break out + break + } + if i == 5 { + log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Still waiting for process exit status")) + } + time.Sleep(time.Second) + } + + // SIGTERM may not work on Windows so we try SIGKILL + if util.IsProcessRunning(cmd.Process) { + log.Debug().Msg(color.HiMagentaString("[HOT RELOAD] Process still hasn't fully exited, attempting SIGKILL")) + if e := cmd.Process.Kill(); e != nil { + log.Error().Err(e).Msg(color.HiMagentaString("[HOT RELOAD] Failed to send SIGKILL")) + } + } + + cmd = nil + } else { + // If `cmd` is nil, we know this is the first time we are starting the process + log.Info().Msg(color.HiMagentaString("[HOT RELOAD] Watching for secret changes...")) + } + + processMutex.Lock() + + if lastUpdateEvent.After(secretsFetchedAt) { + processMutex.Unlock() + return + } + + beingTerminated = false + watcherWaitGroup.Add(1) + + // start the process + log.Info().Msgf(color.GreenString("Injecting %v Infisical secrets into your application process", environmentVariables.SecretsCount)) + + cmd, err = util.RunCommand(commandFlag, args, environmentVariables.Variables, false) + if err != nil { + defer watcherWaitGroup.Done() + util.HandleError(err) + } + + go func() { + defer processMutex.Unlock() + defer watcherWaitGroup.Done() + + exitCode, err := waitForExitCommand(cmd) + + // ignore errors if we are being terminated + if !beingTerminated { + if err != nil { + if strings.HasPrefix(err.Error(), "exec") || strings.HasPrefix(err.Error(), "fork/exec") { + log.Error().Err(err).Msg("Failed to execute command") + } + if err.Error() != ErrManualSignalInterrupt.Error() { + log.Error().Err(err).Msg("Process exited with error") + } + } + + os.Exit(exitCode) + } + }() + } + + recheckSecretsChannel := make(chan bool, 1) + recheckSecretsChannel <- true + + // a simple goroutine that triggers the recheckSecretsChan every watch interval (defaults to 10 seconds) + go func() { + for { + time.Sleep(time.Duration(watchModeInterval) * time.Second) + recheckSecretsChannel <- true + } + }() + + for { + <-recheckSecretsChannel + watchMutex.Lock() + + newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, expandSecrets, token) + if err != nil { + log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets") + continue + } + + if newEnvironmentVariables.ETag != currentETag { + runCommandWithWatcher(newEnvironmentVariables) + } else { + log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process") + } + + watchMutex.Unlock() + + } +} + +func fetchAndFormatSecretsForShell(request models.GetAllSecretsParameters, projectConfigDir string, secretOverriding bool, shouldExpandSecrets bool, token *models.TokenDetails) (models.InjectableEnvironmentResult, error) { + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + request.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + request.UniversalAuthAccessToken = token.Token + } + + secrets, err := util.GetAllEnvironmentVariables(request, projectConfigDir) + + if err != nil { + return models.InjectableEnvironmentResult{}, err + } + + if secretOverriding { + secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL) + } else { + secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED) + } + + if shouldExpandSecrets { + + authParams := models.ExpandSecretsAuthentication{} + + if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER { + authParams.InfisicalToken = token.Token + } else if token != nil && token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + authParams.UniversalAuthAccessToken = token.Token + } + + secrets = util.ExpandSecrets(secrets, authParams, projectConfigDir) + } + + secretsByKey := getSecretsByKeys(secrets) + environmentVariables := make(map[string]string) + + // add all existing environment vars + for _, s := range os.Environ() { + kv := strings.SplitN(s, "=", 2) + key := kv[0] + value := kv[1] + environmentVariables[key] = value + } + + // check to see if there are any reserved key words in secrets to inject + filterReservedEnvVars(secretsByKey) + + // now add infisical secrets + for k, v := range secretsByKey { + environmentVariables[k] = v.Value + } + + env := make([]string, 0, len(environmentVariables)) + for key, value := range environmentVariables { + env = append(env, key+"="+value) + } + + return models.InjectableEnvironmentResult{ + Variables: env, + ETag: util.GenerateETagFromSecrets(secrets), + SecretsCount: len(secretsByKey), + }, nil +} diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index c4bbd0175..1bad5e327 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -104,6 +104,12 @@ type GetAllSecretsParameters struct { Recursive bool } +type InjectableEnvironmentResult struct { + Variables []string + ETag string + SecretsCount int +} + type GetAllFoldersParameters struct { WorkspaceId string Environment string diff --git a/cli/packages/util/exec.go b/cli/packages/util/exec.go new file mode 100644 index 000000000..2cdb50f42 --- /dev/null +++ b/cli/packages/util/exec.go @@ -0,0 +1,92 @@ +package util + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "runtime" + "syscall" +) + +func RunCommand(singleCommand string, args []string, env []string, waitForExit bool) (*exec.Cmd, error) { + var c *exec.Cmd + var err error + + if singleCommand != "" { + c, err = RunCommandFromString(singleCommand, env, waitForExit) + } else { + c, err = RunCommandFromArgs(args, env, waitForExit) + } + + return c, err +} + +func IsProcessRunning(p *os.Process) bool { + err := p.Signal(syscall.Signal(0)) + return err == nil +} + +// For "infisical run -- COMMAND" +func RunCommandFromArgs(args []string, env []string, waitForExit bool) (*exec.Cmd, error) { + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + + err := execCommand(cmd, waitForExit) + + return cmd, err +} + +func execCommand(cmd *exec.Cmd, waitForExit bool) error { + sigChannel := make(chan os.Signal, 1) + signal.Notify(sigChannel) + + if err := cmd.Start(); err != nil { + return err + } + + go func() { + for { + sig := <-sigChannel + _ = cmd.Process.Signal(sig) // process all sigs + } + }() + + if !waitForExit { + return nil + } + + if err := cmd.Wait(); err != nil { + _ = cmd.Process.Signal(os.Kill) + return fmt.Errorf("failed to wait for command termination: %v", err) + } + + waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) + os.Exit(waitStatus.ExitStatus()) + return nil +} + +// For "infisical run --command=COMMAND" +func RunCommandFromString(command string, env []string, waitForExit bool) (*exec.Cmd, error) { + shell := [2]string{"sh", "-c"} + if runtime.GOOS == "windows" { + shell = [2]string{"cmd", "/C"} + } else { + currentShell := os.Getenv("SHELL") + if currentShell != "" { + shell[0] = currentShell + } + } + + cmd := exec.Command(shell[0], shell[1], command) // #nosec G204 nosemgrep: semgrep_configs.prohibit-exec-command + cmd.Env = env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := execCommand(cmd, waitForExit) + return cmd, err +} diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 69a310efa..b758ebd9d 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "math/rand" "os" @@ -298,3 +299,16 @@ func GenerateRandomString(length int) string { } return string(b) } + +func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string { + sortedSecrets := SortSecretsByKeys(secrets) + content := []byte{} + + for _, secret := range sortedSecrets { + content = append(content, []byte(secret.Key)...) + content = append(content, []byte(secret.Value)...) + } + + hash := sha256.Sum256(content) + return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:])) +} diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index 74aa84947..bfa4bd693 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -47,20 +47,20 @@ $ infisical run -- npm run dev Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command. ```bash - # Example - export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. + # Example + export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. ``` Alternatively, you may use service tokens. Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys). + ```bash - # Example - export INFISICAL_TOKEN= + # Example + export INFISICAL_TOKEN= ``` - - + @@ -69,22 +69,30 @@ $ infisical run -- npm run dev To use, simply export this variable in the terminal before running this command. ```bash - # Example - export INFISICAL_DISABLE_UPDATE_CHECK=true + # Example + export INFISICAL_DISABLE_UPDATE_CHECK=true ``` - ### Flags - + + By passing the `watch` flag, you are telling the CLI to watch for changes that happen in your Infisical project. + If secret changes happen, the command you provided will automatically be restarted with the new environment variables attached. + + ```bash + # Example + infisical run --watch -- printenv + ``` + + + Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups. ```bash - # Example - infisical run --project-config-dir=/some-dir -- printenv + # Example + infisical run --project-config-dir=/some-dir -- printenv ``` - @@ -172,3 +180,19 @@ $ infisical run -- npm run dev + + +## Automatically reload command when secrets change + +To automatically reload your command when secrets change, use the `--watch` flag. + +```bash +infisical run --watch -- npm run dev +``` + +This will watch for changes in your secrets and automatically restart your command with the new secrets. +When your command restarts, it will have the new environment variables injeceted into it. + + + Please note that this feature is intended for development purposes. It is not recommended to use this in production environments. Generally it's not recommended to automatically reload your application in production when remote changes are made. + \ No newline at end of file diff --git a/docs/documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit.mdx b/docs/documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit.mdx new file mode 100644 index 000000000..e5411506b --- /dev/null +++ b/docs/documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit.mdx @@ -0,0 +1,61 @@ +--- +title: "Stream to Non-HTTP providers" +description: "How to stream Infisical Audit Logs to Non-HTTP log providers" +--- + + + Audit log streams is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + +This guide will demonstrate how you can send Infisical Audit log streams to storage solutions that do not support direct HTTP-based ingestion, such as AWS S3. +To achieve this, you will learn how you can use a log collector like Fluent Bit to capture and forward logs from Infisical to non-HTTP storage options. +In this pattern, Fluent Bit acts as an intermediary, accepting HTTP log streams from Infisical and transforming them into a format that can be sent to your desired storage provider. + +## Overview + +Log collectors are tools used to collect, analyze, transform, and send logs to storage. +For the purposes of this guide, we will use [Fluent Bit](https://fluentbit.io) as our log collector and send logs from Infisical to AWS S3. +However, this is just a example and you can use any log collector of your choice. + +## Deploy Fluent Bit + +You can deploy Fluent Bit in one of two ways: +1. As a sidecar to your self-hosted Infisical instance +2. As a standalone service in any deployment/compute service (e.g., AWS EC2, ECS, or GCP Compute Engine) + +To view all deployment methods, visit the [Fluent Bit Getting Started guide](https://docs.fluentbit.io/manual/installation/getting-started-with-fluent-bit). + +## Configure Fluent Bit + +To set up Fluent Bit, you'll need to provide a configuration file that establishes an HTTP listener and configures an output to send JSON data to your chosen storage solution. + +The following Fluent Bit configuration sets up an HTTP listener on port `8888` and sends logs to AWS S3: + +```ini +[SERVICE] + Flush 1 + Log_Level info + Daemon off + +[INPUT] + Name http + Listen 0.0.0.0 + Port 8888 + +[OUTPUT] + Name s3 + Match * + bucket my-bucket + region us-west-2 + total_file_size 50M + use_put_object Off + compression gzip + s3_key_format /$TAG/%Y/%m/%d/%H_%M_%S.gz +``` +### Connecting Infisical Audit Log Stream + +Once Fluent Bit is set up and configured, you can point the Infisical [audit log stream](/documentation/platform/audit-log-streams/audit-log-streams) to Fluent Bit's HTTP listener, which will then forward the logs to your chosen provider. +Using this pattern, you are able to send Infisical Audit logs to various providers that do not support HTTP based log ingestion by default. diff --git a/docs/documentation/platform/audit-log-streams.mdx b/docs/documentation/platform/audit-log-streams/audit-log-streams.mdx similarity index 100% rename from docs/documentation/platform/audit-log-streams.mdx rename to docs/documentation/platform/audit-log-streams/audit-log-streams.mdx diff --git a/docs/documentation/platform/audit-logs.mdx b/docs/documentation/platform/audit-logs.mdx index be2381da2..594c1f707 100644 --- a/docs/documentation/platform/audit-logs.mdx +++ b/docs/documentation/platform/audit-logs.mdx @@ -1,5 +1,5 @@ --- -title: "Audit Logs" +title: "Overview" description: "Track evert event action performed within Infisical projects." --- diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx index c37de08ff..225b884cb 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -58,7 +58,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. - ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png) @@ -116,7 +116,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png) + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. @@ -125,7 +125,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. - ![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png) + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) @@ -133,11 +133,11 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. This will allow you see the expiration time of the lease or delete a lease before it's set time to live. -![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png) +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. -![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png) +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret diff --git a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx index cdcb6ce2b..0b2897790 100644 --- a/docs/documentation/platform/dynamic-secrets/elastic-search.mdx +++ b/docs/documentation/platform/dynamic-secrets/elastic-search.mdx @@ -23,7 +23,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. - ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png) @@ -99,7 +99,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png) + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. @@ -108,7 +108,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. - ![Provision Lease](/images/platform/dynamic-secrets/lease-values-elastic-search.png) + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) @@ -116,11 +116,11 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. This will allow you see the expiration time of the lease or delete a lease before it's set time to live. -![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png) +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. -![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png) +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret diff --git a/docs/documentation/platform/dynamic-secrets/mongo-db.mdx b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx new file mode 100644 index 000000000..f34d578dc --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/mongo-db.mdx @@ -0,0 +1,116 @@ +--- +title: "Mongo DB" +description: "Learn how to dynamically generate Mongo DB Database user credentials." +--- + +The Infisical Mongo DB dynamic secret allows you to generate Mongo DB Database credentials on demand based on configured role. + + + + If your using Mongo Atlas, please use [Atlas Dynamic Secret](./mongo-atlas) as MongoDB commands are not supported by atlas. + + +## Prerequisite +Create a user with the required permission in your MongoDB instance. This user will be used to create new accounts on-demand. + +## Set up Dynamic Secrets with Mongo DB + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-mongodb.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret + + + + Database host URL. + + + + Database port number. If your Mongo DB is cluster you can omit this. + + + + Username of the admin user that will be used to create dynamic secrets + + + + Password of the admin user that will be used to create dynamic secrets + + + + Name of the database for which you want to create dynamic secrets + + + + Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role. + - Enum: `atlasAdmin` `backup` `clusterMonitor` `dbAdmin` `dbAdminAnyDatabase` `enableSharding` `read` `readAnyDatabase` `readWrite` `readWriteAnyDatabase` ``. + + + + A CA may be required if your DB requires it for incoming connections. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-mongodb.png) + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certificate. + + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete a lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + diff --git a/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx new file mode 100644 index 000000000..f8649b727 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/rabbit-mq.mdx @@ -0,0 +1,116 @@ +--- +title: "RabbitMQ" +description: "Learn how to dynamically generate RabbitMQ user credentials." +--- + +The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credentials on demand based on configured role. + +## Prerequisites + +1. Ensure that the `management` plugin is enabled on your RabbitMQ instance. This is required for the dynamic secret to work. + + +## Set up Dynamic Secrets with RabbitMQ + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-rabbit-mq.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret. + + + + Your RabbitMQ host. This must be in HTTP format. _(Example: http://your-cluster-ip)_ + + + + The port that the RabbitMQ management plugin is listening on. This is `15672` by default. + + + + The name of the virtual host that the user will be assigned to. This defaults to `/`. + + + + The permissions that the user will have on the virtual host. This defaults to `.*`. + + The three permission fields all take a regular expression _(regex)_, that should match resource names for which the user is granted read / write / configuration permissions + + + + + The username of the user that will be used to provision new dynamic secret leases. + + + + The password of the user that will be used to provision new dynamic secret leases. + + + + A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. + + + ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png) + + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certificate. + + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete a lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx index 1270e6959..cb2e6a17e 100644 --- a/docs/documentation/platform/dynamic-secrets/redis.mdx +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -16,7 +16,7 @@ Create a user with the required permission in your Redis instance. This user wil Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. - ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-redis.png) @@ -78,7 +78,7 @@ Create a user with the required permission in your Redis instance. This user wil When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. - ![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png) + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. @@ -87,7 +87,7 @@ Create a user with the required permission in your Redis instance. This user wil Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. - ![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png) + ![Provision Lease](/images/platform/dynamic-secrets/lease-values.png) @@ -95,11 +95,11 @@ Create a user with the required permission in your Redis instance. This user wil Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. This will allow you see the expiration time of the lease or delete a lease before it's set time to live. -![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png) +![Provision Lease](/images/platform/dynamic-secrets/lease-data.png) ## Renew Leases To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. -![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png) +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png) Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx index ac9b4f2d7..776d175a4 100644 --- a/docs/documentation/platform/identities/oidc-auth/general.mdx +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -93,7 +93,12 @@ In the following steps, we explore how to create and use identities to access th - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. + + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx index 4a9e2c671..47352a339 100644 --- a/docs/documentation/platform/identities/oidc-auth/github.mdx +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -92,8 +92,8 @@ In the following steps, we explore how to create and use identities to access th - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. - If you are unsure about what to configure for the subject, audience, and claims fields you can use [github/actions-oidc-debugger](https://github.com/github/actions-oidc-debugger) to get the appropriate values. Alternatively, you can fetch the JWT from the workflow and inspect the fields manually. + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. diff --git a/docs/documentation/platform/pki/certificate-templates.mdx b/docs/documentation/platform/pki/certificate-templates.mdx deleted file mode 100644 index a52bf4364..000000000 --- a/docs/documentation/platform/pki/certificate-templates.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: "Certificate Templates" -sidebarTitle: "Certificate Templates" -description: "Learn how to use certificate templates to enforce policies." ---- - -## Concept - -In order to ensure your certificates follow certain policies, you can use certificate templates during the issuance and signing flows. - -A certificate template is linked to a certificate authority. It contains custom policies for certificate fields, allowing you to define rules based on your security policies. - -## Workflow - -The typical workflow for using certificate templates consists of the following steps: - -1. Creating a certificate template attached to an existing CA along with defining custom rules for certificate fields. -2. Selecting the certificate template during the creation of new certificates. - - - Note that this workflow can be executed via the Infisical UI or manually such - as via API. - - -## Guide to using Certificate Templates - -In the following steps, we explore how to issue a X.509 certificate using a certificate template. - - - - - - - To create a certificate template, head to your Project > Internal PKI > Certificate Templates and press **Create Certificate Template**. - - ![certificate-template create template dashboard](/images/platform/pki/certificate-template/create-template-dashboard.png) - - Here, set the **Issuing CA** to the CA you want to issue certificates under when the certificate template is used. - - ![certificate-template create template modal](/images/platform/pki/certificate-template/create-template-form.png) - - Here's some guidance on each field: - - Template Name: A descriptive name for the certificate template. - - Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template. - - Certificate Collection: The collection where certificates issued with this template will be added. - - Common Name (CN): The regular expression used to validate the common name in certificate requests. - - Alternative Names (SANs): The regular expression used to validate subject alternative names in certificate requests. - - TTL: The maximum Time-to-Live (TTL) for certificates issued using this template. - - - - Once you have created the certificate template from step 1, you can select it when issuing certificates. - - ![certificate-template select template](/images/platform/pki/certificate-template/select-template.png) - - - - - - - To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint. - - ### Sample request - - ```bash Request - curl --request POST \ - --url https://app.infisical.com/api/v1/pki/certificate-templates \ - --header 'Content-Type: application/json' \ - --data '{ - "caId": "", - "pkiCollectionId": "", - "name": "", - "commonName": "", - "subjectAlternativeName": "", - "ttl": "" - }' - ``` - - ### Sample response - - ```bash Response - { - "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "caId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "name": "certificate-template-1", - "commonName": "", - ... - } - ``` - - - To use the certificate template, attach the certificate template ID when invoking the API endpoint for [issuing](/api-reference/endpoints/certificates/issue-certificate) or [signing](/api-reference/endpoints/certificates/sign-certificate) new certificates. - - ### Sample request - - ```bash Request - curl --request POST \ - --url https://app.infisical.com/api/v1/pki/certificates/issue-certificate \ - --header 'Content-Type: application/json' \ - --data '{ - "certificateTemplateId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", - "friendlyName": "my-new-certificate", - "commonName": "CERT", - ... - }' - ``` - - - - - diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index 41a5cb8c9..a4f1ba02c 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -25,7 +25,7 @@ graph TD The typical workflow for managing certificates consists of the following steps: -1. Issuing a certificate under an intermediate CA with details like name and validity period. +1. Issuing a certificate under an intermediate CA with details like name and validity period. As part of certificate issuance, you can either issue a certificate directly from a CA or do it via a certificate template. 2. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow, you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed data structure issued by a CA containing a list of revoked certificates to check if a certificate has been revoked. @@ -43,28 +43,51 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. + + A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection. + + With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like `.*.acme.com` or perhaps that the max TTL cannot be more than 1 year. + + Head to your Project > Certificate Authorities > Your Issuing CA and create a certificate template. + + ![pki certificate template modal](/images/platform/pki/certificate/cert-template-modal.png) + + Here's some guidance on each field: + + - Template Name: A name for the certificate template. + - Issuing CA: The Certificate Authority (CA) that will issue certificates based on this template. + - Certificate Collection (Optional): The certificate collection that certificates should be added to when issued under the template. + - Common Name (CN): A regular expression used to validate the common name in certificate requests. + - Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests. + - TTL: The maximum Time-to-Live (TTL) for certificates issued using this template. + - To create a certificate, head to your Project > Internal PKI > Certificates and press **Create Certificate**. + To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section. - ![pki issue certificate](/images/platform/pki/cert-issue.png) + ![pki issue certificate](/images/platform/pki/certificate/cert-issue.png) - Here, set the **CA** to the CA you want to issue the certificate under and fill out details for the certificate. + Here, set the **Certificate Template** to the template from step 1 and fill out the rest of the details for the certificate to be issued. - ![pki issue certificate modal](/images/platform/pki/cert-issue-modal.png) + ![pki issue certificate modal](/images/platform/pki/certificate/cert-issue-modal.png) Here's some guidance on each field: - - Issuing CA: The CA under which to issue the certificate. - Friendly Name: A friendly name for the certificate; this is only for display and defaults to the common name of the certificate if left empty. - Common Name (CN): The (common) name for the certificate like `service.acme.com`. - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. - TTL: The lifetime of the certificate in seconds. - + + + Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** + and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. + + That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. + Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**. - ![pki certificate body](/images/platform/pki/cert-body.png) + ![pki certificate body](/images/platform/pki/certificate/cert-body.png) Make sure to download and store the **Private Key** in a secure location as it will only be displayed once at the time of certificate issuance. @@ -74,16 +97,54 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. - To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, + + + + A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA and can also be bound to a certificate collection for alerting such that any certificate issued under the template is automatically added to the collection. + + With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year. + + To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA. + + ### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificate-templates' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "caId": "", + "name": "My Certificate Template", + "commonName": ".*.acme.com", + "subjectAlternativeName": ".*.acme.com", + "ttl": "1y", + }' + ``` + + ### Sample response + + ```bash Response + { + id: "...", + caId: "...", + name: "...", + commonName: "...", + subjectAlternativeName: "...", + ttl: "...", + } + ``` + + + To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, specifying the issuing CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//issue-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/issue-certificate' \ --header 'Content-Type: application/json' \ --data-raw '{ - "commonName": "My Certificate", + "certificateTemplateId": "", + "commonName": "service.acme.com", "ttl": "1y", }' ``` @@ -100,18 +161,26 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. } ``` + + Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** + and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. + + That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. + + Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time. - If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA. + If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-certificate) API endpoint, specifying the issuing CA. ### Sample request ```bash Request - curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca//sign-certificate' \ + curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/sign-certificate' \ --header 'Content-Type: application/json' \ --data-raw '{ + "certificateTemplateId": "", "csr": "...", "ttl": "1y", }' @@ -128,7 +197,8 @@ In the following steps, we explore how to issue a X.509 certificate under a CA. serialNumber: "..." } ``` - + + diff --git a/docs/documentation/platform/pki/est.mdx b/docs/documentation/platform/pki/est.mdx index ee66c32b6..a31a5672e 100644 --- a/docs/documentation/platform/pki/est.mdx +++ b/docs/documentation/platform/pki/est.mdx @@ -26,7 +26,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. ## Guide to configuring EST -1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates). +1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificates#guide-to-issuing-certificates). 2. Proceed to the certificate template's enrollment settings ![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png) diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx new file mode 100644 index 000000000..c02e477c6 --- /dev/null +++ b/docs/documentation/platform/pki/pki-issuer.mdx @@ -0,0 +1,250 @@ +--- +title: "Kubernetes Issuer" +sidebarTitle: "Certificates for Kubernetes" +description: "Learn how to automatically provision and manage TLS certificates for in Kubernetes using Infisical PKI" +--- + +## Concept + +The Infisical PKI Issuer is an installable Kubernetes [cert-manager](https://cert-manager.io/) controller that uses Infisical PKI to sign certificate requests. The issuer is perfect for getting X.509 certificates for ingresses and other Kubernetes resources and capable of automatically renewing certificates as needed. + +As part of the workflow, you install `cert-manager`, the Infisical PKI Issuer, and configure resources to represent the connection details to your Infisical PKI and the certificates you wish to issue. Each issued certificate and corresponding private key is made available in a Kubernetes secret. + +We recommend reading the [cert-manager documentation](https://cert-manager.io/docs/) for a fuller understanding of all the moving parts. + +## Workflow + +A typical workflow for using the Infisical PKI Issuer to issue certificates for your Kubernetes resources consists of the following steps: + +1. Creating a machine identity in Infisical. +2. Creating a Kubernetes secret to store the credentials of the machine identity. +3. Installing `cert-manager` into your Kubernetes cluster. +4. Installing the Infisical PKI Issuer controller into your Kubernetes cluster. +5. Creating an `Issuer` or `ClusterIssuer` resource in your Kubernetes cluster to represent the Infisical PKI issuer you wish to use. +6. Creating a `Certificate` resource in your Kubernetes cluster to represent a certificate you wish to issue. As part of this step, you specify the Kubernetes `Secret` to create and store the issued certificate and private key. +7. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`. + +## Guide + +In the following steps, we explore how to install the Infisical PKI Issuer using [kubectl](https://github.com/kubernetes/kubectl) and use it to obtain certificates for your Kubernetes resources. + + + + + Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth. + + By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the Infisical PKI Issuer to authenticate with Infisical; this will be useful in steps 4 and 5. + + + Currently, the Infisical PKI Issuer only supports authenticating with Infisical via the [Universal Auth](/documentation/platform/identities/universal-auth) authentication method. + + We're planning to add support for [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) in the near future. + + + + Install `cert-manager` into your Kubernetes cluster by following the instructions [here](https://cert-manager.io/docs/installation/) or by running the following command: + + ```bash + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml + ``` + + + Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command: + + ```bash + kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml + ``` + + + Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical. + + + + ```bash + kubectl create secret generic issuer-infisical-client-secret \ + --namespace \ + --from-literal=clientSecret= + ``` + + + ```yaml secret-issuer.yaml + apiVersion: v1 + kind: Secret + metadata: + name: issuer-infisical-client-secret + namespace: + data: + clientSecret: + ``` + + ```bash + kubectl apply -f secret-issuer.yaml + ``` + + + + + Next, create the Infisical PKI Issuer by filling out `url`, `clientId`, either `caId` or `certificateTemplateId`, and applying the following configuration file for the `Issuer` resource. + This configuration file specifies the connection details to your Infisical PKI CA to be used for issuing certificates. + + ```yaml infisical-issuer.yaml + apiVersion: infisical-issuer.infisical.com/v1alpha1 + kind: Issuer + metadata: + name: issuer-infisical + namespace: + spec: + url: "https://app.infisical.com" # the URL of your Infisical instance + caId: # the ID of the CA you want to use to issue certificates + certificateTemplateId: # the ID of the certificate template you want to use to issue certificates against + authentication: + universalAuth: + clientId: # the Client ID from step 1 + secretRef: # reference to the Secret created in step 4 + name: "issuer-infisical-client-secret" + key: "clientSecret" + ``` + + ``` + kubectl apply -f infisical-issuer.yaml + ``` + + + The Infisical PKI Issuer supports issuing certificates against a specific CA or a specific certificate template. + + For this reason, you should only fill in the `caId` or the `certificateTemplateId` field but not both. + + We recommend using the `certificateTemplateId` field to issue certificates against a specific [certificate template](/documentation/platform/pki/certificate-templates) + since templates let you enforce constraints on issued certificates and may have alerting policies bound to them. + + + You can check that the issuer was created successfully by running the following command: + + ```bash + kubectl get issuers.infisical-issuer.infisical.com -n -o wide + ``` + + ```bash + NAME AGE + issuer-infisical 21h + ``` + + + An `Issuer` is a namespaced resource, and it is not possible to issue certificates from an `Issuer` in a different namespace. + This means you will need to create an `Issuer` in each namespace you wish to obtain `Certificates` in. + + If you want to create a single `Issuer` that can be consumed in multiple namespaces, you should consider creating a `ClusterIssuer` resource. This is almost identical to the `Issuer` resource, however is non-namespaced so it can be used to issue `Certificates` across all namespaces. + + You can read more about the `Issuer` and `ClusterIssuer` resources [here](https://cert-manager.io/docs/configuration/). + + + + + Finally, create a `Certificate` by applying the following configuration file. + This configuration file specifies the details of the (end-entity/leaf) certificate to be issued. + + ```yaml certificate-issuer.yaml + apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: certificate-by-issuer + namespace: + spec: + commonName: certificate-by-issuer.example.com # the common name for the certificate + secretName: certificate-by-issuer # the name of the Kubernetes Secret to create and store the certificate and private key in + issuerRef: + name: issuer-infisical + group: infisical-issuer.infisical.com + kind: Issuer + privateKey: # the algorithm and key size to use + algorithm: ECDSA + size: 256 + duration: 48h # the ttl for the certificate + renewBefore: 12h # the time before the certificate expiry that the certificate should be automatically renewed + ``` + + The above sample configuration file specifies a certificate to be issued with the common name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry. + The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`. + + Note that the full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec). + + You can check that the certificate was created successfully by running the following command: + + ```bash + kubectl get certificates -n -o wide + ``` + + ```bash + NAME READY SECRET ISSUER STATUS AGE + certificate-by-issuer True certificate-by-issuer issuer-infisical Certificate is up to date and has not expired 20h + ``` + + + Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command: + + ```bash + kubectl get secret certificate-by-issuer -n + ``` + + ```bash + NAME TYPE DATA AGE + certificate-by-issuer kubernetes.io/tls 2 26h + ``` + + We can `describe` the secret to get more information about it: + + ```bash + kubectl describe secret certificate-by-issuer -n default + ``` + + ```bash + Name: certificate-by-issuer + Namespace: default + Labels: controller.cert-manager.io/fao=true + Annotations: cert-manager.io/alt-names: + cert-manager.io/certificate-name: certificate-by-issuer + cert-manager.io/common-name: certificate-by-issuer.example.com + cert-manager.io/ip-sans: + cert-manager.io/issuer-group: infisical-issuer.infisical.com + cert-manager.io/issuer-kind: Issuer + cert-manager.io/issuer-name: issuer-infisical + cert-manager.io/uri-sans: + + Type: kubernetes.io/tls + + Data + ==== + ca.crt: 1306 bytes + tls.crt: 2380 bytes + tls.key: 227 bytes + ``` + + Here, `ca.crt` is the Root CA certificate, `tls.crt` is the requested certificate followed by the certificate chain, and `tls.key` is the private key for the certificate. + + We can decode the certificate and print it out using `openssl`: + + ```bash + kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout + ``` + + In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources. + + + +## FAQ + + + + The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec). + + + Currently, not all fields are supported by the Infisical PKI Issuer. + + + + Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as + specified in the corresponding `Certificate` resource. + + You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec). + + \ No newline at end of file diff --git a/docs/documentation/platform/pki/private-ca.mdx b/docs/documentation/platform/pki/private-ca.mdx index 0baa13abb..d7f3f896c 100644 --- a/docs/documentation/platform/pki/private-ca.mdx +++ b/docs/documentation/platform/pki/private-ca.mdx @@ -66,6 +66,7 @@ consisting of an (optional) root CA and an intermediate CA. - State or Province Name: The state or province. - Locality Name: The city or locality. - Common Name: The name of the CA. + - Require Template for Certificate Issuance: Whether or not certificates for this CA can only be issued through certificate templates (recommended). The Organization, Country, State or Province Name, Locality Name, and Common Name make up the **Distinguished Name (DN)** or **subject** of the CA. diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index cbd5a7d0e..fb4d4418f 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -62,7 +62,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." ![Azure SAML edit certificate signing option](../../../images/sso/azure/edit-saml-certificate-2.png) - + In the **Set up Single Sign-On with SAML** screen, copy the **Login URL** and **SAML Certificate** to use when finishing configuring Azure SAML in Infisical. ![Azure SAML identity provider values 1](../../../images/sso/azure/idp-values.png) @@ -115,4 +115,4 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. - `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) - \ No newline at end of file + diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png deleted file mode 100644 index 537f20e73..000000000 Binary files a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png index 8d0fd3ecc..537f20e73 100644 Binary files a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png and b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png b/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png new file mode 100644 index 000000000..41508b465 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-input-modal-rabbit-mq.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png deleted file mode 100644 index a2fd14d41..000000000 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png index e97554415..3d2e32e8a 100644 Binary files a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-mongodb.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-mongodb.png new file mode 100644 index 000000000..be000f753 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-mongodb.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-mongodb.png b/docs/images/platform/dynamic-secrets/dynamic-secret-mongodb.png new file mode 100644 index 000000000..d3a804f8f Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-mongodb.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-rabbit-mq-modal.png b/docs/images/platform/dynamic-secrets/dynamic-secret-rabbit-mq-modal.png new file mode 100644 index 000000000..885a17a88 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-rabbit-mq-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-data-redis.png b/docs/images/platform/dynamic-secrets/lease-data-redis.png deleted file mode 100644 index f3fd51637..000000000 Binary files a/docs/images/platform/dynamic-secrets/lease-data-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/lease-data.png b/docs/images/platform/dynamic-secrets/lease-data.png index aecd8c11d..9562da1b5 100644 Binary files a/docs/images/platform/dynamic-secrets/lease-data.png and b/docs/images/platform/dynamic-secrets/lease-data.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png b/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png deleted file mode 100644 index 7d5685a81..000000000 Binary files a/docs/images/platform/dynamic-secrets/lease-values-elastic-search.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/lease-values-redis.png b/docs/images/platform/dynamic-secrets/lease-values-redis.png deleted file mode 100644 index 95d4a4ffd..000000000 Binary files a/docs/images/platform/dynamic-secrets/lease-values-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/lease-values.png b/docs/images/platform/dynamic-secrets/lease-values.png index d552845f8..962bd76ec 100644 Binary files a/docs/images/platform/dynamic-secrets/lease-values.png and b/docs/images/platform/dynamic-secrets/lease-values.png differ diff --git a/docs/images/platform/dynamic-secrets/provision-lease-redis.png b/docs/images/platform/dynamic-secrets/provision-lease-redis.png deleted file mode 100644 index f5237d058..000000000 Binary files a/docs/images/platform/dynamic-secrets/provision-lease-redis.png and /dev/null differ diff --git a/docs/images/platform/dynamic-secrets/provision-lease.png b/docs/images/platform/dynamic-secrets/provision-lease.png index f144a5ae2..96b0505b9 100644 Binary files a/docs/images/platform/dynamic-secrets/provision-lease.png and b/docs/images/platform/dynamic-secrets/provision-lease.png differ diff --git a/docs/images/platform/pki/cert-body.png b/docs/images/platform/pki/cert-body.png deleted file mode 100644 index 8ed67a7ec..000000000 Binary files a/docs/images/platform/pki/cert-body.png and /dev/null differ diff --git a/docs/images/platform/pki/cert-issue-modal.png b/docs/images/platform/pki/cert-issue-modal.png deleted file mode 100644 index 1516ab1cb..000000000 Binary files a/docs/images/platform/pki/cert-issue-modal.png and /dev/null differ diff --git a/docs/images/platform/pki/cert-issue.png b/docs/images/platform/pki/cert-issue.png deleted file mode 100644 index 6b3e5887b..000000000 Binary files a/docs/images/platform/pki/cert-issue.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate-template/create-template-dashboard.png b/docs/images/platform/pki/certificate-template/create-template-dashboard.png deleted file mode 100644 index 6f193effa..000000000 Binary files a/docs/images/platform/pki/certificate-template/create-template-dashboard.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate-template/create-template-form.png b/docs/images/platform/pki/certificate-template/create-template-form.png deleted file mode 100644 index e69791edd..000000000 Binary files a/docs/images/platform/pki/certificate-template/create-template-form.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate-template/select-template.png b/docs/images/platform/pki/certificate-template/select-template.png deleted file mode 100644 index c10031a45..000000000 Binary files a/docs/images/platform/pki/certificate-template/select-template.png and /dev/null differ diff --git a/docs/images/platform/pki/certificate/cert-body.png b/docs/images/platform/pki/certificate/cert-body.png new file mode 100644 index 000000000..8c1433b54 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-body.png differ diff --git a/docs/images/platform/pki/certificate/cert-issue-modal.png b/docs/images/platform/pki/certificate/cert-issue-modal.png new file mode 100644 index 000000000..f73462c8f Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-issue-modal.png differ diff --git a/docs/images/platform/pki/certificate/cert-issue.png b/docs/images/platform/pki/certificate/cert-issue.png new file mode 100644 index 000000000..614271d19 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-issue.png differ diff --git a/docs/images/platform/pki/certificate/cert-template-modal.png b/docs/images/platform/pki/certificate/cert-template-modal.png new file mode 100644 index 000000000..f3995b6e4 Binary files /dev/null and b/docs/images/platform/pki/certificate/cert-template-modal.png differ diff --git a/docs/images/platform/pki/certs.png b/docs/images/platform/pki/certs.png deleted file mode 100644 index 4e1b49959..000000000 Binary files a/docs/images/platform/pki/certs.png and /dev/null differ diff --git a/docs/images/platform/pki/est/template-enroll-hover.png b/docs/images/platform/pki/est/template-enroll-hover.png index 8b13cdd60..cc0f6f658 100644 Binary files a/docs/images/platform/pki/est/template-enroll-hover.png and b/docs/images/platform/pki/est/template-enroll-hover.png differ diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index d53c557fb..467789647 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -30,6 +30,7 @@ Prerequisites: "ssm:DeleteParameter", "ssm:GetParameters", "ssm:GetParametersByPath", + "ssm:DescribeParameters", "ssm:DeleteParameters", "ssm:AddTagsToResource", // if you need to add tags to secrets "kms:ListKeys", // if you need to specify the KMS key diff --git a/docs/mint.json b/docs/mint.json index b56493e01..3fb414e0c 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -108,7 +108,7 @@ "documentation/platform/pki/overview", "documentation/platform/pki/private-ca", "documentation/platform/pki/certificates", - "documentation/platform/pki/certificate-templates", + "documentation/platform/pki/pki-issuer", "documentation/platform/pki/est", "documentation/platform/pki/alerting" ] @@ -130,11 +130,18 @@ "documentation/platform/access-controls/temporary-access", "documentation/platform/access-controls/access-requests", "documentation/platform/pr-workflows", - "documentation/platform/audit-logs", "documentation/platform/audit-log-streams", "documentation/platform/groups" ] }, + { + "group": "Audit Logs", + "pages": [ + "documentation/platform/audit-logs", + "documentation/platform/audit-log-streams/audit-log-streams", + "documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit" + ] + }, { "group": "Secret Rotation", "pages": [ @@ -157,8 +164,10 @@ "documentation/platform/dynamic-secrets/redis", "documentation/platform/dynamic-secrets/aws-elasticache", "documentation/platform/dynamic-secrets/elastic-search", + "documentation/platform/dynamic-secrets/rabbit-mq", "documentation/platform/dynamic-secrets/aws-iam", - "documentation/platform/dynamic-secrets/mongo-atlas" + "documentation/platform/dynamic-secrets/mongo-atlas", + "documentation/platform/dynamic-secrets/mongo-db" ] }, { @@ -698,7 +707,7 @@ "api-reference/endpoints/certificate-authorities/import-cert", "api-reference/endpoints/certificate-authorities/issue-cert", "api-reference/endpoints/certificate-authorities/sign-cert", - "api-reference/endpoints/certificate-authorities/crls" + "api-reference/endpoints/certificate-authorities/crl" ] }, { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3148f5da2..95104cdf1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "frontend", "dependencies": { "@casl/ability": "^6.5.0", "@casl/react": "^3.1.0", diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index fcec4f2ef..27c7c87d2 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -24,6 +24,7 @@ import { SRP1DTO, SRPR1Res, TOauthTokenExchangeDTO, + UserAgentType, VerifyMfaTokenDTO, VerifyMfaTokenRes, VerifySignupInviteDTO @@ -60,7 +61,10 @@ export const useLogin1 = () => { }); }; -export const selectOrganization = async (data: { organizationId: string }) => { +export const selectOrganization = async (data: { + organizationId: string; + userAgent?: UserAgentType; +}) => { const { data: res } = await apiRequest.post<{ token: string }>( "/api/v3/auth/select-organization", data @@ -71,11 +75,14 @@ export const selectOrganization = async (data: { organizationId: string }) => { export const useSelectOrganization = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (details: { organizationId: string }) => { + mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => { const data = await selectOrganization(details); - SecurityClient.setToken(data.token); - SecurityClient.setProviderAuthToken(""); + // If a custom user agent is set, then this session is meant for another consuming application, not the web application. + if (!details.userAgent) { + SecurityClient.setToken(data.token); + SecurityClient.setProviderAuthToken(""); + } return data; }, diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index 3664e8e96..f51f7b091 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -145,3 +145,7 @@ export type IssueBackupPrivateKeyDTO = { export type GetBackupEncryptedPrivateKeyDTO = { verificationToken: string; }; + +export enum UserAgentType { + CLI = "cli" +} diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 5c0ff4caa..4993a8fdc 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -8,4 +8,4 @@ export { useSignIntermediate, useUpdateCa } from "./mutations"; -export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries"; +export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates,useGetCaCrls, useGetCaCsr } from "./queries"; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index 48652f3fd..5e668ecef 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -43,8 +43,9 @@ export const useUpdateCa = () => { } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body); return ca; }, - onSuccess: (_, { projectSlug }) => { + onSuccess: ({ id }, { projectSlug }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug })); + queryClient.invalidateQueries(caKeys.getCaById(id)); } }); }; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 996043f19..a1e633776 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TCertificateTemplate } from "../certificateTemplates/types"; import { TCertificateAuthority } from "./types"; export const caKeys = { @@ -11,6 +12,7 @@ export const caKeys = { getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"], + getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"], getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"] }; @@ -90,3 +92,16 @@ export const useGetCaCrls = (caId: string) => { enabled: Boolean(caId) }); }; + +export const useGetCaCertTemplates = (caId: string) => { + return useQuery({ + queryKey: caKeys.getCaCertTemplates(caId), + queryFn: async () => { + const { data } = await apiRequest.get<{ + certificateTemplates: TCertificateTemplate[]; + }>(`/api/v1/pki/ca/${caId}/certificate-templates`); + return data; + }, + enabled: Boolean(caId) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index e09ae16b8..52e363fa7 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -19,6 +19,7 @@ export type TCertificateAuthority = { notAfter?: string; notBefore?: string; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; activeCaCertId?: string; createdAt: string; updatedAt: string; @@ -37,12 +38,14 @@ export type TCreateCaDTO = { notAfter?: string; maxPathLength: number; keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; }; export type TUpdateCaDTO = { projectSlug: string; caId: string; status?: CaStatus; + requireTemplateForIssuance?: boolean; }; export type TDeleteCaDTO = { diff --git a/frontend/src/hooks/api/certificateTemplates/mutations.tsx b/frontend/src/hooks/api/certificateTemplates/mutations.tsx index 101507af0..f633c0332 100644 --- a/frontend/src/hooks/api/certificateTemplates/mutations.tsx +++ b/frontend/src/hooks/api/certificateTemplates/mutations.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { caKeys } from "../ca/queries"; import { workspaceKeys } from "../workspace/queries"; import { certTemplateKeys } from "./queries"; import { @@ -23,8 +24,9 @@ export const useCreateCertTemplate = () => { ); return certificateTemplate; }, - onSuccess: (_, { projectId }) => { + onSuccess: ({ caId }, { projectId }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; @@ -40,22 +42,25 @@ export const useUpdateCertTemplate = () => { return certificateTemplate; }, - onSuccess: (_, { projectId, id }) => { + onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; export const useDeleteCertTemplate = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async (data) => { - return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); + const { data: certificateTemplate } = await apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`); + return certificateTemplate; }, - onSuccess: (_, { projectId, id }) => { + onSuccess: ({ caId }, { projectId, id }) => { queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId)); queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id)); + queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId)); } }); }; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index b79c8f02e..32c9b15d0 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -22,7 +22,9 @@ export enum DynamicSecretProviders { Redis = "redis", AwsElastiCache = "aws-elasticache", MongoAtlas = "mongo-db-atlas", - ElasticSearch = "elastic-search" + ElasticSearch = "elastic-search", + MongoDB = "mongo-db", + RabbitMq = "rabbit-mq" } export enum SqlProviders { @@ -117,6 +119,24 @@ export type TDynamicSecretProvider = }[]; }; } + | { + type: DynamicSecretProviders.MongoDB; + inputs: { + host: string; + port?: number; + database: string; + username: string; + password: string; + ca?: string | undefined; + roles: ( + | { + databaseName: string; + roleName: string; + } + | string + )[]; + }; + } | { type: DynamicSecretProviders.ElasticSearch; inputs: { @@ -124,7 +144,6 @@ export type TDynamicSecretProvider = port: number; ca?: string | undefined; roles: string[]; - auth: | { type: "user"; @@ -137,6 +156,27 @@ export type TDynamicSecretProvider = apiKeyId: string; }; }; + } + | { + type: DynamicSecretProviders.RabbitMq; + inputs: { + host: string; + port: number; + + username: string; + password: string; + + tags: string[]; + virtualHost: { + name: string; + permissions: { + configure: string; + write: string; + read: string; + }; + }; + ca?: string; + }; }; export type TCreateDynamicSecretDTO = { diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index cf953664d..13ec3e414 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -16,6 +16,7 @@ import { Button, Spinner } from "@app/components/v2"; import { SessionStorageKeys } from "@app/const"; import { useUser } from "@app/context"; import { useGetOrganizations, useLogoutUser, useSelectOrganization } from "@app/hooks/api"; +import { UserAgentType } from "@app/hooks/api/auth/types"; import { Organization } from "@app/hooks/api/types"; import { getAuthToken, isLoggedIn } from "@app/reactQuery"; import { navigateUserToOrg } from "@app/views/Login/Login.utils"; @@ -68,7 +69,10 @@ export default function LoginPage() { return; } - const { token } = await selectOrg.mutateAsync({ organizationId: organization.id }); + const { token } = await selectOrg.mutateAsync({ + organizationId: organization.id, + userAgent: callbackPort ? UserAgentType.CLI : undefined + }); if (callbackPort) { const privateKey = localStorage.getItem("PRIVATE_KEY"); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 30663d470..fdfb17e0d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -1,12 +1,13 @@ import { useEffect } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2"; +import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; @@ -258,7 +259,19 @@ export const IdentityOidcAuthForm = ({ control={control} name="boundSubject" render={({ field, fieldState: { error } }) => ( - + This field supports glob patterns} + > + + + } + > )} @@ -267,7 +280,19 @@ export const IdentityOidcAuthForm = ({ control={control} name="boundAudiences" render={({ field, fieldState: { error } }) => ( - + This field supports glob patterns} + > + + + } + > )} @@ -282,6 +307,16 @@ export const IdentityOidcAuthForm = ({ This field supports glob patterns} + > + + + ) : undefined + } isError={Boolean(error)} errorText={error?.message} > diff --git a/frontend/src/views/Project/CaPage/CaPage.tsx b/frontend/src/views/Project/CaPage/CaPage.tsx index 23f94570c..4afb537f0 100644 --- a/frontend/src/views/Project/CaPage/CaPage.tsx +++ b/frontend/src/views/Project/CaPage/CaPage.tsx @@ -22,6 +22,7 @@ import { usePopUp } from "@app/hooks/usePopUp"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; +import { CertificateTemplatesSection } from "../CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection"; import { CaCertificatesSection, CaCrlsSection, @@ -125,6 +126,7 @@ export const CaPage = withProjectPermission(
+
diff --git a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx index 94eae3e6b..9d5d20337 100644 --- a/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx +++ b/frontend/src/views/Project/CaPage/components/CaDetailsSection.tsx @@ -1,4 +1,4 @@ -import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; @@ -33,6 +33,28 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {

CA Details

+ + {(isAllowed) => { + return ( + + { + e.stopPropagation(); + handlePopUpOpen("ca", { + caId: ca.id + }); + }} + > + + + + ); + }} +
@@ -115,6 +137,12 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => { {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}

+
+

Template Issuance Required

+

+ {ca.requireTemplateForIssuance ? "True" : "False"} +

+
{ca.status === CaStatus.ACTIVE && ( { // const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || ""); + const { mutateAsync: createMutateAsync } = useCreateCa(); + const { mutateAsync: updateMutateAsync } = useUpdateCa(); const { control, @@ -110,7 +114,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: ca.commonName, notAfter: ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "", maxPathLength: ca.maxPathLength ? String(ca.maxPathLength) : "", - keyAlgorithm: ca.keyAlgorithm + keyAlgorithm: ca.keyAlgorithm, + requireTemplateForIssuance: ca.requireTemplateForIssuance }); } else { reset({ @@ -124,7 +129,8 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { commonName: "", notAfter: getDateTenYearsFromToday(), maxPathLength: "-1", - keyAlgorithm: CertKeyAlgorithm.RSA_2048 + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + requireTemplateForIssuance: true }); } }, [ca]); @@ -140,31 +146,43 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { province, notAfter, maxPathLength, - keyAlgorithm + keyAlgorithm, + requireTemplateForIssuance }: FormData) => { try { if (!currentWorkspace?.slug) return; - - await createMutateAsync({ - projectSlug: currentWorkspace.slug, - type, - friendlyName, - commonName, - organization, - ou, - country, - province, - locality, - notAfter, - maxPathLength: Number(maxPathLength), - keyAlgorithm - }); + + if (ca) { + // update + await updateMutateAsync({ + projectSlug: currentWorkspace.slug, + caId: ca.id, + requireTemplateForIssuance + }); + } else { + // create + await createMutateAsync({ + projectSlug: currentWorkspace.slug, + type, + friendlyName, + commonName, + organization, + ou, + country, + province, + locality, + notAfter, + maxPathLength: Number(maxPathLength), + keyAlgorithm, + requireTemplateForIssuance + }); + } reset(); handlePopUpToggle("ca", false); createNotification({ - text: "Successfully created CA", + text: `Successfully ${ca ? "updated" : "created"} CA`, type: "success" }); } catch (err) { @@ -186,6 +204,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { >
+ {ca && ( + + + + )} { )} /> - {!ca && ( -
- - -
- )} + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Require Template for Certificate Issuance

+
+
+ ); + }} + /> +
+ + +
diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx index d4e7e957d..35c034f34 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaTable.tsx @@ -3,7 +3,6 @@ import { faBan, faCertificate, faEllipsis, - faEye, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -155,28 +154,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { )}
)} - - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("ca", { - caId: ca.id - }); - }} - disabled={!isAllowed} - icon={} - > - View CA - - )} - {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( { @@ -14,7 +14,7 @@ export const CertificatesTab = () => { exit={{ opacity: 0, translateX: 30 }} > - + {/* */} ); diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx index 40645a07e..a5520c3c3 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -218,7 +218,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { } errorText={error?.message} isError={Boolean(error)} - className="mt-4" isRequired > + + )} ( {...field} onValueChange={(e) => onChange(e)} className="w-full" + isDisabled > {(cas || []).map(({ id, type, dn }) => ( diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx index 4c3ca73fa..b1cce918f 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateTemplatesSection.tsx @@ -1,9 +1,13 @@ +/** + * TODO (dangtony98): Reevaluate if this component should be in main + * CertificateTab or under CA page in the future. + */ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2"; +import { DeleteActionModal, IconButton, UpgradePlanModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteCertTemplate } from "@app/hooks/api"; @@ -12,7 +16,11 @@ import { CertificateTemplateEnrollmentModal } from "./CertificateTemplateEnrollm import { CertificateTemplateModal } from "./CertificateTemplateModal"; import { CertificateTemplatesTable } from "./CertificateTemplatesTable"; -export const CertificateTemplatesSection = () => { +type Props = { + caId: string; +} + +export const CertificateTemplatesSection = ({ caId }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "certificateTemplate", "deleteCertificateTemplate", @@ -50,28 +58,30 @@ export const CertificateTemplatesSection = () => { }; return ( -
-
-

Certificate Templates

+
+
+

Certificate Templates

{(isAllowed) => ( - + + )}
- - +
+ +
+ void; }; -export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); +export const CertificateTemplatesTable = ({ handlePopUpOpen, caId }: Props) => { const { subscription } = useSubscription(); - const { data, isLoading } = useListWorkspaceCertificateTemplates({ - workspaceId: currentWorkspace?.id ?? "" - }); + + const { data, isLoading } = useGetCaCertTemplates(caId); return (
@@ -54,7 +55,6 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { Name - Certificate Authority @@ -65,13 +65,12 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { return ( {certificateTemplate.name} - {certificateTemplate.caName}
- +
@@ -143,7 +142,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => { {!isLoading && !data?.certificateTemplates?.length && ( - + )}
diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 71b25e63c..90eb74012 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { DiRedis } from "react-icons/di"; -import { SiApachecassandra, SiElasticsearch, SiMongodb } from "react-icons/si"; +import { SiApachecassandra, SiElasticsearch, SiMongodb, SiRabbitmq } from "react-icons/si"; import { faAws } from "@fortawesome/free-brands-svg-icons"; import { faDatabase } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -14,6 +14,8 @@ import { AwsIamInputForm } from "./AwsIamInputForm"; import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; +import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; +import { RabbitMqInputForm } from "./RabbitMqInputForm"; import { RedisInputForm } from "./RedisInputForm"; import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; @@ -61,10 +63,20 @@ const DYNAMIC_SECRET_LIST = [ provider: DynamicSecretProviders.MongoAtlas, title: "Mongo Atlas" }, + { + icon: , + provider: DynamicSecretProviders.MongoDB, + title: "Mongo DB" + }, { icon: , provider: DynamicSecretProviders.ElasticSearch, title: "Elastic Search" + }, + { + icon: , + provider: DynamicSecretProviders.RabbitMq, + title: "RabbitMQ" } ]; @@ -252,6 +264,42 @@ export const CreateDynamicSecretForm = ({ /> )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.MongoDB && ( + + + + )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.RabbitMq && ( + + + + )} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx new file mode 100644 index 000000000..d24850ca8 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/MongoDBInputForm.tsx @@ -0,0 +1,338 @@ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +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 { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + provider: z.object({ + host: z.string().toLowerCase().min(1), + port: z.coerce.number().optional(), + database: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + ca: z.string().optional(), + roles: z + .object({ + roleName: z.string().min(1) + }) + .array() + .min(1) + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + 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") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const MongoDBDatabaseInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + setValue, + getValues, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + roles: [{ roleName: "readWrite" }] + } + } + }); + + const roleFields = useFieldArray({ + control, + name: "provider.roles" + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { + type: DynamicSecretProviders.MongoDB, + inputs: { + ...provider, + port: provider?.port ? provider.port : undefined, + roles: provider.roles.map((el) => el.roleName) + } + }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ {roleFields.fields.map(({ id: roleFieldId }, i) => ( +
+
+ ( + + + + )} + /> +
+ { + const roles = getValues("provider.roles"); + if (roles && roles?.length > 1) { + roleFields.remove(i); + } else { + setValue("provider.roles", [{ roleName: "" }]); + } + }} + > + + +
+ ))} +
+ +
+
+
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx new file mode 100644 index 000000000..dae0cd478 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/RabbitMqInputForm.tsx @@ -0,0 +1,421 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +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 { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + provider: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), // important: this is the management plugin port + + username: z.string(), + password: z.string(), + + tags: z.array(z.string().trim()), + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }), + ca: z.string().optional() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + 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") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const RabbitMqInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + port: 15672, + virtualHost: { + name: "/", + permissions: { + read: ".*", + write: ".*", + configure: ".*" + } + }, + tags: [] + } + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.RabbitMq, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + const selectedTags = watch("provider.tags"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ +
+ +

Select which tag(s) to assign the users provisioned by Infisical.

+

+ There is a wide range of in-built roles in RabbitMQ. Some include, + management, policymaker, monitoring, administrator.
+ + + + Read more about management tags here + + + + . +

+

+ You can also assign custom roles by providing the name of the custom role in + the input field. +

+
+ } + /> +
+ {selectedTags.map((_, i) => ( + ( + +
+ + { + if (selectedTags && selectedTags?.length > 1) { + setValue( + "provider.tags", + selectedTags.filter((__, idx) => idx !== i) + ); + } + }} + > + + +
+
+ )} + /> + ))} +
+
+
+ +
+
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+ +
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 9b3d069c2..85be20fc1 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -58,7 +58,8 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { if ( provider === DynamicSecretProviders.SqlDatabase || provider === DynamicSecretProviders.Cassandra || - provider === DynamicSecretProviders.MongoAtlas + provider === DynamicSecretProviders.MongoAtlas || + provider === DynamicSecretProviders.MongoDB ) { const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string }; return ( @@ -139,6 +140,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.RabbitMq) { + const { DB_USERNAME, DB_PASSWORD } = data as { + DB_USERNAME: string; + DB_PASSWORD: string; + }; + + return ( +
+ + +
+ ); + } + if (provider === DynamicSecretProviders.ElasticSearch) { const { DB_USERNAME, DB_PASSWORD } = data as { DB_USERNAME: string; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx index f0a4fd957..8f95bcc94 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx @@ -9,6 +9,8 @@ import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm"; import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm"; import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm"; import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm"; +import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm"; +import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm"; import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm"; import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm"; @@ -165,6 +167,41 @@ export const EditDynamicSecretForm = ({ /> )} + {dynamicSecretDetails?.type === DynamicSecretProviders.MongoDB && ( + + + + )} + + {dynamicSecretDetails?.type === DynamicSecretProviders.RabbitMq && ( + + + + )} ); }; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx new file mode 100644 index 000000000..2c160fd17 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretMongoDBForm.tsx @@ -0,0 +1,349 @@ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +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 { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + inputs: z + .object({ + host: z.string().toLowerCase().min(1), + port: z.coerce.number().optional(), + database: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + ca: z.string().optional(), + roles: z + .object({ + roleName: z.string().min(1) + }) + .array() + .min(1) + }) + .partial(), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }) + .nullable(), + newName: z + .string() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .optional() +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; + +export const EditDynamicSecretMongoDBForm = ({ + onClose, + dynamicSecret, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + getValues, + setValue, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + values: { + defaultTTL: dynamicSecret.defaultTTL, + maxTTL: dynamicSecret.maxTTL, + newName: dynamicSecret.name, + inputs: { + ...(dynamicSecret.inputs as TForm["inputs"]), + roles: (dynamicSecret.inputs as { roles: string[] }).roles?.map((roleName) => ({ + roleName + })) + } + } + }); + + const roleFields = useFieldArray({ + control, + name: "inputs.roles" + }); + + const updateDynamicSecret = useUpdateDynamicSecret(); + + const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { + // wait till previous request is finished + if (updateDynamicSecret.isLoading) return; + try { + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs: { + ...inputs, + port: inputs?.port ? inputs.port : undefined, + roles: inputs?.roles?.map((el) => el.roleName) + }, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update dynamic secret" + }); + } + }; + + return ( +
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ {roleFields.fields.map(({ id: roleFieldId }, i) => ( +
+
+ ( + + + + )} + /> +
+ { + const roles = getValues("inputs.roles"); + if (roles && roles?.length > 1) { + roleFields.remove(i); + } else { + setValue("inputs.roles", [{ roleName: "" }]); + } + }} + > + + +
+ ))} +
+ +
+
+
+ ( + + + + )} + /> +
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx new file mode 100644 index 000000000..a28c257b0 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretRabbitMqForm.tsx @@ -0,0 +1,421 @@ +import { Controller, useForm } from "react-hook-form"; +import Link from "next/link"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +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 { useUpdateDynamicSecret } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + inputs: z.object({ + host: z.string().trim().min(1), + port: z.coerce.number(), // important: this is the management plugin port + + username: z.string(), + password: z.string(), + + tags: z.array(z.string().trim()), + virtualHost: z.object({ + name: z.string().trim().min(1), + permissions: z.object({ + read: z.string().trim().min(1), + write: z.string().trim().min(1), + configure: z.string().trim().min(1) + }) + }), + ca: z.string().optional() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onClose: () => void; + dynamicSecret: TDynamicSecret & { inputs: unknown }; + secretPath: string; + environment: string; + projectSlug: string; +}; +export const EditDynamicSecretRabbitMqForm = ({ + onClose, + dynamicSecret, + secretPath, + environment, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit, + setValue, + watch + } = useForm({ + resolver: zodResolver(formSchema), + values: { + defaultTTL: dynamicSecret.defaultTTL, + maxTTL: dynamicSecret.maxTTL, + newName: dynamicSecret.name, + inputs: { + ...(dynamicSecret.inputs as TForm["inputs"]) + } + } + }); + + const updateDynamicSecret = useUpdateDynamicSecret(); + + const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { + // wait till previous request is finished + if (updateDynamicSecret.isLoading) return; + try { + await updateDynamicSecret.mutateAsync({ + name: dynamicSecret.name, + path: secretPath, + projectSlug, + environmentSlug: environment, + data: { + maxTTL: maxTTL || undefined, + defaultTTL, + inputs, + newName: newName === dynamicSecret.name ? undefined : newName + } + }); + onClose(); + createNotification({ + type: "success", + text: "Successfully updated dynamic secret" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update dynamic secret" + }); + } + }; + + const selectedTags = watch("inputs.tags"); + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ +
+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ +
+ } + /> +
+ {selectedTags.map((_, i) => ( + ( + +
+ + { + if (selectedTags && selectedTags?.length > 1) { + setValue( + "inputs.tags", + selectedTags.filter((__, idx) => idx !== i) + ); + } + }} + > + + +
+
+ )} + /> + ))} +
+
+
+ +
+
+ ( + + + + )} + /> +
+
+
+
+
+ + +
+ +
+ ); +};