Compare commits

..

56 Commits

Author SHA1 Message Date
97c069bc0f fix typo of bulk to batch 2025-02-23 15:09:40 +09:00
4a44b7857e docs: go sdk bulk create secrets 2025-02-21 04:50:20 +04:00
2ba834b036 Merge pull request #3136 from Infisical/aws-secrets-manager-many-to-one-update
Fix: AWS Secrets Manager Remove Deletion of other Secrets from Many-to-One Mapping
2025-02-20 12:29:01 -08:00
db7a6f3530 fix: remove deletion of other secrets from many-to-one mapping 2025-02-20 12:21:52 -08:00
d80a104f7b Merge pull request #3079 from Infisical/feat/kmip-client-management
feat: kmip
2025-02-20 17:53:15 +08:00
d980d471e8 Merge pull request #3132 from Infisical/doc/add-caching-reference-to-go-sdk
doc: add caching reference for go sdk
2025-02-19 22:00:27 -05:00
3583a09ab5 Merge pull request #3125 from Infisical/fix-org-select-none
Fix failing redirect to create new organization page on no organizations
2025-02-19 13:53:33 -08:00
86b6d23f34 Fix lint issue 2025-02-19 10:27:55 -08:00
2c31ac0569 misc: finalized KMIP icon 2025-02-20 02:11:04 +08:00
d6c1b8e30c misc: addressed comments 2025-02-20 01:46:50 +08:00
0d4d73b61d misc: update default usage to be numerical 2025-02-19 20:16:45 +08:00
198b607e2e doc: add caching reference for go sdk 2025-02-19 20:11:14 +08:00
f0e6bcef9b fix: addressed rabbit findings 2025-02-19 17:16:02 +08:00
3a5a88467d Merge remote-tracking branch 'origin' into fix-org-select-none 2025-02-18 18:55:08 -08:00
012f265363 Fix lint issues 2025-02-18 18:42:39 -08:00
823bf134dd Fix case where user can get stuck in a deleted organization if they were prev logged into it 2025-02-18 18:35:22 -08:00
1f5a73047d Merge pull request #3067 from Infisical/doc/added-docs-for-egress-ips
doc: added section for egress ips
2025-02-18 19:04:03 -05:00
0366df6e19 Update terraform-cloud.mdx 2025-02-18 17:48:56 -05:00
c77e0c0666 Merge pull request #3127 from Infisical/oidc-guide-tf-cloud
OIDC Guide for Terraform cloud <> Infisical
2025-02-18 17:27:15 -05:00
8e70731c4c add terraform cloud oidc docs 2025-02-18 17:23:03 -05:00
1db8c9ea29 doc: finish up KMIP 2025-02-19 02:59:04 +08:00
21c6700db2 Merge pull request #3106 from thomas-infisical/terraform-provider
docs: add ephemeral resources documentation to Terraform guide
2025-02-18 09:55:42 -08:00
619062033b docs: enhance Terraform integration guide with ephemeral resources and best practices 2025-02-18 17:40:24 +01:00
92b3b9157a feat: added kmip server to CLI 2025-02-18 20:59:14 +08:00
f6cd78e078 misc: added exception for kmip 2025-02-18 17:41:34 +08:00
36973b1b5c Merge pull request #3078 from akhilmhdh/feat/batch-upsert
Batch upsert operation
2025-02-18 09:32:01 +05:30
=
1ca578ee03 feat: updated based on review feedback 2025-02-18 00:45:53 +05:30
=
8a7f7ac9fd feat: added test cases for bulk update 2025-02-18 00:23:10 +05:30
=
049fd8e769 feat: added upsert and ignore to bulk update 2025-02-18 00:23:10 +05:30
8e2cce865a Merge remote-tracking branch 'origin/main' into feat/kmip-client-management 2025-02-18 02:50:12 +08:00
943c2b0e69 misc: finalize KMIP management 2025-02-18 02:22:59 +08:00
8518fed726 Merge remote-tracking branch 'origin' into fix-org-select-none 2025-02-17 09:20:29 -08:00
5148435f5f Move user to create org screen on no org 2025-02-17 09:20:19 -08:00
2c825616a6 Merge pull request #3124 from Infisical/update-hardware
Update hardware for infisical
2025-02-17 09:55:35 -05:00
febbd4ade5 update hardware for infisical 2025-02-17 09:51:30 -05:00
603b740bbe misc: migrated KMIP PKI to be scoped at the org level 2025-02-17 15:54:02 +08:00
874dc01692 Update upgrading-infisical.mdx 2025-02-14 23:43:15 -05:00
b44b8bf647 Merge pull request #3121 from Infisical/upgrade-infisical-dcos
Docs for new upgrade process
2025-02-14 20:41:41 -05:00
258e561b84 add upgrade docs 2025-02-14 20:41:05 -05:00
5802638fc4 Merge pull request #3120 from Infisical/is-pending-fixes
Improvement: Use isPending Over isLoading
2025-02-14 09:10:05 -08:00
e2e7004583 improvement: additional isPending fix 2025-02-14 08:59:12 -08:00
7826324435 fix: use isPending over isLoading 2025-02-14 08:54:37 -08:00
af652f7e52 feat: kmip poc done 2025-02-14 23:55:57 +08:00
b8a07979c3 misc: audit logs 2025-02-12 01:34:29 +08:00
292c9051bd feat: kmip create and get 2025-02-11 23:32:11 +08:00
77fac45df1 misc: reordered migration 2025-02-07 01:39:48 +08:00
0ab90383c2 Merge remote-tracking branch 'origin/main' into feat/kmip-client-management 2025-02-07 01:26:04 +08:00
a3acfa65a2 feat: finished up client cert generation 2025-02-07 01:24:32 +08:00
0269f57768 feat: completed kmip server cert config 2025-02-06 22:36:31 +08:00
9f9ded5102 misc: initial instance KMIP PKI setup 2025-02-06 02:57:40 +08:00
8b315c946c feat: added kmip to project roles section 2025-02-04 19:29:34 +08:00
dd9a7755bc feat: completed KMIP client overview 2025-02-04 18:49:48 +08:00
64c2fba350 feat: added list support and overview page 2025-02-04 02:44:59 +08:00
c7f80f7d9e feat: kmip client backend setup 2025-02-04 01:34:30 +08:00
ecf2cb6e51 misc: made improvements to wording 2025-01-30 13:09:29 +08:00
1e5a9a6020 doc: added section for egress ips 2025-01-30 03:36:46 +08:00
117 changed files with 6307 additions and 756 deletions

View File

@ -535,6 +535,107 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }]
);
});
test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => {
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
secretPath: path,
mode: "upsert",
secrets: Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: secret.comment
}))
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const secrets = await getSecrets(seedData1.environment.slug, path);
expect(secrets).toEqual(
expect.arrayContaining(
Array.from(Array(5)).map((_e, i) =>
expect.objectContaining({
secretKey: `BULK-${secret.key}-${i + 1}`,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
);
});
test("Bulk upsert secrets in path multiple paths", async () => {
const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: "comment",
secretPath: secretTestCases[0].path
}));
const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`,
secretValue: "update-value",
secretComment: "comment",
secretPath: secretTestCases[1].path
}));
const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets];
const updateSharedSecRes = await testServer.inject({
method: "PATCH",
url: `/api/v3/secrets/batch/raw`,
headers: {
authorization: `Bearer ${authToken}`
},
body: {
workspaceId: seedData1.projectV3.id,
environment: seedData1.environment.slug,
mode: "upsert",
secrets: testSecrets
}
});
expect(updateSharedSecRes.statusCode).toBe(200);
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
expect(updateSharedSecPayload).toHaveProperty("secrets");
// bulk ones should exist
const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path);
expect(firstBatchSecretsOnInfisical).toEqual(
expect.arrayContaining(
firstBatchSecrets.map((el) =>
expect.objectContaining({
secretKey: el.secretKey,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path);
expect(secondBatchSecretsOnInfisical).toEqual(
expect.arrayContaining(
secondBatchSecrets.map((el) =>
expect.objectContaining({
secretKey: el.secretKey,
secretValue: "update-value",
type: SecretType.Shared
})
)
)
);
await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey })));
});
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
await Promise.all(
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))

View File

@ -16,6 +16,9 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
@ -126,6 +129,11 @@ declare module "fastify" {
isUserCompleted: string;
providerAuthToken: string;
};
kmipUser: {
projectId: string;
clientId: string;
name: string;
};
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
@ -218,11 +226,14 @@ declare module "fastify" {
totp: TTotpServiceFactory;
appConnection: TAppConnectionServiceFactory;
secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer
store: {
user: Pick<TUserDALFactory, "findById">;
kmipClient: Pick<TKmipClientDALFactory, "findByProjectAndClientId">;
};
}
}

View File

@ -143,6 +143,18 @@ import {
TInternalKms,
TInternalKmsInsert,
TInternalKmsUpdate,
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate,
TKmipClients,
TKmipClientsInsert,
TKmipClientsUpdate,
TKmipOrgConfigs,
TKmipOrgConfigsInsert,
TKmipOrgConfigsUpdate,
TKmipOrgServerCertificates,
TKmipOrgServerCertificatesInsert,
TKmipOrgServerCertificatesUpdate,
TKmsKeys,
TKmsKeysInsert,
TKmsKeysUpdate,
@ -902,5 +914,21 @@ declare module "knex/types/tables" {
TAppConnectionsUpdate
>;
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
[TableName.KmipClient]: KnexOriginal.CompositeTableType<TKmipClients, TKmipClientsInsert, TKmipClientsUpdate>;
[TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType<
TKmipOrgConfigs,
TKmipOrgConfigsInsert,
TKmipOrgConfigsUpdate
>;
[TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType<
TKmipOrgServerCertificates,
TKmipOrgServerCertificatesInsert,
TKmipOrgServerCertificatesUpdate
>;
[TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType<
TKmipClientCertificates,
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
}
}

View File

@ -0,0 +1,108 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
if (!hasKmipClientTable) {
await knex.schema.createTable(TableName.KmipClient, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.specificType("permissions", "text[]");
t.string("description");
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
}
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
if (!hasKmipOrgPkiConfig) {
await knex.schema.createTable(TableName.KmipOrgConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.unique("orgId");
t.string("caKeyAlgorithm").notNullable();
t.datetime("rootCaIssuedAt").notNullable();
t.datetime("rootCaExpiration").notNullable();
t.string("rootCaSerialNumber").notNullable();
t.binary("encryptedRootCaCertificate").notNullable();
t.binary("encryptedRootCaPrivateKey").notNullable();
t.datetime("serverIntermediateCaIssuedAt").notNullable();
t.datetime("serverIntermediateCaExpiration").notNullable();
t.string("serverIntermediateCaSerialNumber");
t.binary("encryptedServerIntermediateCaCertificate").notNullable();
t.binary("encryptedServerIntermediateCaChain").notNullable();
t.binary("encryptedServerIntermediateCaPrivateKey").notNullable();
t.datetime("clientIntermediateCaIssuedAt").notNullable();
t.datetime("clientIntermediateCaExpiration").notNullable();
t.string("clientIntermediateCaSerialNumber").notNullable();
t.binary("encryptedClientIntermediateCaCertificate").notNullable();
t.binary("encryptedClientIntermediateCaChain").notNullable();
t.binary("encryptedClientIntermediateCaPrivateKey").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.KmipOrgConfig);
}
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
if (!hasKmipOrgServerCertTable) {
await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("commonName").notNullable();
t.string("altNames").notNullable();
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
t.binary("encryptedCertificate").notNullable();
t.binary("encryptedChain").notNullable();
});
}
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (!hasKmipClientCertTable) {
await knex.schema.createTable(TableName.KmipClientCertificates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("kmipClientId").notNullable();
t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE");
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
if (hasKmipOrgPkiConfig) {
await knex.schema.dropTable(TableName.KmipOrgConfig);
await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig);
}
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
if (hasKmipOrgServerCertTable) {
await knex.schema.dropTable(TableName.KmipOrgServerCertificates);
}
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
if (hasKmipClientCertTable) {
await knex.schema.dropTable(TableName.KmipClientCertificates);
}
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
if (hasKmipClientTable) {
await knex.schema.dropTable(TableName.KmipClient);
}
}

View File

@ -45,6 +45,10 @@ export * from "./incident-contacts";
export * from "./integration-auths";
export * from "./integrations";
export * from "./internal-kms";
export * from "./kmip-client-certificates";
export * from "./kmip-clients";
export * from "./kmip-org-configs";
export * from "./kmip-org-server-certificates";
export * from "./kms-key-versions";
export * from "./kms-keys";
export * from "./kms-root-config";

View File

@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const KmipClientCertificatesSchema = z.object({
id: z.string().uuid(),
kmipClientId: z.string().uuid(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date()
});
export type TKmipClientCertificates = z.infer<typeof KmipClientCertificatesSchema>;
export type TKmipClientCertificatesInsert = Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>;
export type TKmipClientCertificatesUpdate = Partial<
Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>
>;

View File

@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const KmipClientsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
permissions: z.string().array().nullable().optional(),
description: z.string().nullable().optional(),
projectId: z.string()
});
export type TKmipClients = z.infer<typeof KmipClientsSchema>;
export type TKmipClientsInsert = Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>;
export type TKmipClientsUpdate = Partial<Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,39 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmipOrgConfigsSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
caKeyAlgorithm: z.string(),
rootCaIssuedAt: z.date(),
rootCaExpiration: z.date(),
rootCaSerialNumber: z.string(),
encryptedRootCaCertificate: zodBuffer,
encryptedRootCaPrivateKey: zodBuffer,
serverIntermediateCaIssuedAt: z.date(),
serverIntermediateCaExpiration: z.date(),
serverIntermediateCaSerialNumber: z.string().nullable().optional(),
encryptedServerIntermediateCaCertificate: zodBuffer,
encryptedServerIntermediateCaChain: zodBuffer,
encryptedServerIntermediateCaPrivateKey: zodBuffer,
clientIntermediateCaIssuedAt: z.date(),
clientIntermediateCaExpiration: z.date(),
clientIntermediateCaSerialNumber: z.string(),
encryptedClientIntermediateCaCertificate: zodBuffer,
encryptedClientIntermediateCaChain: zodBuffer,
encryptedClientIntermediateCaPrivateKey: zodBuffer,
createdAt: z.date(),
updatedAt: z.date()
});
export type TKmipOrgConfigs = z.infer<typeof KmipOrgConfigsSchema>;
export type TKmipOrgConfigsInsert = Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>;
export type TKmipOrgConfigsUpdate = Partial<Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmipOrgServerCertificatesSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
commonName: z.string(),
altNames: z.string(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date(),
encryptedCertificate: zodBuffer,
encryptedChain: zodBuffer
});
export type TKmipOrgServerCertificates = z.infer<typeof KmipOrgServerCertificatesSchema>;
export type TKmipOrgServerCertificatesInsert = Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>;
export type TKmipOrgServerCertificatesUpdate = Partial<
Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>
>;

View File

@ -132,7 +132,11 @@ export enum TableName {
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections",
SecretSync = "secret_syncs"
SecretSync = "secret_syncs",
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -9,6 +9,8 @@ import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerKmipRouter } from "./kmip-router";
import { registerKmipSpecRouter } from "./kmip-spec-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOidcRouter } from "./oidc-router";
@ -110,4 +112,12 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
});
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
await server.register(
async (kmipRouter) => {
await kmipRouter.register(registerKmipRouter);
await kmipRouter.register(registerKmipSpecRouter, { prefix: "/spec" });
},
{ prefix: "/kmip" }
);
};

View File

@ -0,0 +1,428 @@
import ms from "ms";
import { z } from "zod";
import { KmipClientsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators";
const KmipClientResponseSchema = KmipClientsSchema.pick({
projectId: true,
name: true,
id: true,
description: true,
permissions: true
});
export const registerKmipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/clients",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
projectId: z.string(),
name: z.string().trim().min(1),
description: z.string().optional(),
permissions: z.nativeEnum(KmipPermission).array()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.createKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.CREATE_KMIP_CLIENT,
metadata: {
id: kmipClient.id,
name: kmipClient.name,
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
}
}
});
return kmipClient;
}
});
server.route({
method: "PATCH",
url: "/clients/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
name: z.string().trim().min(1),
description: z.string().optional(),
permissions: z.nativeEnum(KmipPermission).array()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.updateKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.UPDATE_KMIP_CLIENT,
metadata: {
id: kmipClient.id,
name: kmipClient.name,
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
}
}
});
return kmipClient;
}
});
server.route({
method: "DELETE",
url: "/clients/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.deleteKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.DELETE_KMIP_CLIENT,
metadata: {
id: kmipClient.id
}
}
});
return kmipClient;
}
});
server.route({
method: "GET",
url: "/clients/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: KmipClientResponseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmipClient = await server.services.kmip.getKmipClient({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: kmipClient.projectId,
event: {
type: EventType.GET_KMIP_CLIENT,
metadata: {
id: kmipClient.id
}
}
});
return kmipClient;
}
});
server.route({
method: "GET",
url: "/clients",
config: {
rateLimit: readLimit
},
schema: {
description: "List KMIP clients",
querystring: z.object({
projectId: z.string(),
offset: z.coerce.number().min(0).optional().default(0),
limit: z.coerce.number().min(1).max(100).optional().default(100),
orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name),
orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC),
search: z.string().trim().optional()
}),
response: {
200: z.object({
kmipClients: KmipClientResponseSchema.array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.GET_KMIP_CLIENTS,
metadata: {
ids: kmipClients.map((key) => key.id)
}
}
});
return { kmipClients, totalCount };
}
});
server.route({
method: "POST",
url: "/clients/:id/certificates",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
serialNumber: z.string(),
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificate = await server.services.kmip.createKmipClientCertificate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
clientId: req.params.id,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: certificate.projectId,
event: {
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE,
metadata: {
clientId: req.params.id,
serialNumber: certificate.serialNumber,
ttl: req.body.ttl,
keyAlgorithm: req.body.keyAlgorithm
}
}
});
return certificate;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
}),
response: {
200: z.object({
serverCertificateChain: z.string(),
clientCertificateChain: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const chains = await server.services.kmip.setupOrgKmip({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.SETUP_KMIP,
metadata: {
keyAlgorithm: req.body.caKeyAlgorithm
}
}
});
return chains;
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
serverCertificateChain: z.string(),
clientCertificateChain: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const kmip = await server.services.kmip.getOrgKmip({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_KMIP,
metadata: {
id: kmip.id
}
}
});
return kmip;
}
});
server.route({
method: "POST",
url: "/server-registration",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
hostnamesOrIps: validateAltNamesField,
commonName: z.string().trim().min(1).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048),
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
clientCertificateChain: z.string(),
certificateChain: z.string(),
certificate: z.string(),
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const configs = await server.services.kmip.registerServer({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.REGISTER_KMIP_SERVER,
metadata: {
serverCertificateSerialNumber: configs.serverCertificateSerialNumber,
hostnamesOrIps: req.body.hostnamesOrIps,
commonName: req.body.commonName ?? "kmip-server",
keyAlgorithm: req.body.keyAlgorithm,
ttl: req.body.ttl
}
}
});
return configs;
}
});
};

View File

@ -0,0 +1,477 @@
import z from "zod";
import { KmsKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
server.decorateRequest("kmipUser", null);
server.addHook("onRequest", async (req) => {
const clientId = req.headers["x-kmip-client-id"] as string;
const projectId = req.headers["x-kmip-project-id"] as string;
const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string;
const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string;
if (!serverCertSerialNumber) {
throw new ForbiddenRequestError({
message: "Missing server certificate serial number from request"
});
}
if (!clientCertSerialNumber) {
throw new ForbiddenRequestError({
message: "Missing client certificate serial number from request"
});
}
if (!clientId) {
throw new ForbiddenRequestError({
message: "Missing client ID from request"
});
}
if (!projectId) {
throw new ForbiddenRequestError({
message: "Missing project ID from request"
});
}
// TODO: assert that server certificate used is not revoked
// TODO: assert that client certificate used is not revoked
const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId);
if (!kmipClient) {
throw new NotFoundError({
message: "KMIP client cannot be found."
});
}
if (kmipClient.orgId !== req.permission.orgId) {
throw new ForbiddenRequestError({
message: "Client specified in the request does not belong in the organization"
});
}
req.kmipUser = {
projectId,
clientId,
name: kmipClient.name
};
});
server.route({
method: "POST",
url: "/create",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for creating managed objects",
body: z.object({
algorithm: z.nativeEnum(SymmetricEncryption)
}),
response: {
200: KmsKeysSchema
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.create({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
algorithm: req.body.algorithm
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_CREATE,
metadata: {
id: object.id,
algorithm: req.body.algorithm
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/get",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for getting managed objects",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
value: z.string(),
algorithm: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.get({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_GET,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/get-attributes",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for getting attributes of managed object",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
algorithm: z.string(),
isActive: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.getAttributes({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/destroy",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for destroying managed objects",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.destroy({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_DESTROY,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/activate",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for activating managed object",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
isActive: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.activate({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_ACTIVATE,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/revoke",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for revoking managed object",
body: z.object({
id: z.string()
}),
response: {
200: z.object({
id: z.string(),
updatedAt: z.date()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.revoke({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.body.id
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_REVOKE,
metadata: {
id: object.id
}
}
});
return object;
}
});
server.route({
method: "POST",
url: "/locate",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for locating managed objects",
response: {
200: z.object({
objects: z
.object({
id: z.string(),
name: z.string(),
isActive: z.boolean(),
algorithm: z.string(),
createdAt: z.date(),
updatedAt: z.date()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const objects = await server.services.kmipOperation.locate({
...req.kmipUser,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_LOCATE,
metadata: {
ids: objects.map((obj) => obj.id)
}
}
});
return {
objects
};
}
});
server.route({
method: "POST",
url: "/register",
config: {
rateLimit: writeLimit
},
schema: {
description: "KMIP endpoint for registering managed object",
body: z.object({
key: z.string(),
name: z.string(),
algorithm: z.nativeEnum(SymmetricEncryption)
}),
response: {
200: z.object({
id: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const object = await server.services.kmipOperation.register({
...req.kmipUser,
...req.body,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
projectId: req.kmipUser.projectId,
actor: {
type: ActorType.KMIP_CLIENT,
metadata: {
clientId: req.kmipUser.clientId,
name: req.kmipUser.name
}
},
event: {
type: EventType.KMIP_OPERATION_REGISTER,
metadata: {
id: object.id,
algorithm: req.body.algorithm,
name: object.name
}
}
});
return object;
}
});
};

View File

@ -21,6 +21,8 @@ import {
TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types";
import { KmipPermission } from "../kmip/kmip-enum";
export type TListProjectAuditLogDTO = {
filter: {
userAgentType?: UserAgentType;
@ -39,7 +41,14 @@ export type TListProjectAuditLogDTO = {
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
actor:
| UserActor
| IdentityActor
| ServiceActor
| ScimClientActor
| PlatformActor
| UnknownUserActor
| KmipClientActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -252,7 +261,26 @@ export enum EventType {
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client",
UPDATE_KMIP_CLIENT = "update-kmip-client",
DELETE_KMIP_CLIENT = "delete-kmip-client",
GET_KMIP_CLIENT = "get-kmip-client",
GET_KMIP_CLIENTS = "get-kmip-clients",
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate",
SETUP_KMIP = "setup-kmip",
GET_KMIP = "get-kmip",
REGISTER_KMIP_SERVER = "register-kmip-server",
KMIP_OPERATION_CREATE = "kmip-operation-create",
KMIP_OPERATION_GET = "kmip-operation-get",
KMIP_OPERATION_DESTROY = "kmip-operation-destroy",
KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes",
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register"
}
interface UserActorMetadata {
@ -275,6 +303,11 @@ interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
interface KmipClientActorMetadata {
clientId: string;
name: string;
}
interface UnknownUserActorMetadata {}
export interface UserActor {
@ -292,6 +325,11 @@ export interface PlatformActor {
metadata: PlatformActorMetadata;
}
export interface KmipClientActor {
type: ActorType.KMIP_CLIENT;
metadata: KmipClientActorMetadata;
}
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
metadata: UnknownUserActorMetadata;
@ -307,7 +345,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@ -352,6 +390,7 @@ interface CreateSecretBatchEvent {
secrets: Array<{
secretId: string;
secretKey: string;
secretPath?: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
}>;
@ -374,8 +413,14 @@ interface UpdateSecretBatchEvent {
type: EventType.UPDATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>;
secretPath?: string;
secrets: Array<{
secretId: string;
secretKey: string;
secretVersion: number;
secretMetadata?: TSecretMetadata;
secretPath?: string;
}>;
};
}
@ -2084,6 +2129,139 @@ interface OidcGroupMembershipMappingRemoveUserEvent {
};
}
interface CreateKmipClientEvent {
type: EventType.CREATE_KMIP_CLIENT;
metadata: {
name: string;
id: string;
permissions: KmipPermission[];
};
}
interface UpdateKmipClientEvent {
type: EventType.UPDATE_KMIP_CLIENT;
metadata: {
name: string;
id: string;
permissions: KmipPermission[];
};
}
interface DeleteKmipClientEvent {
type: EventType.DELETE_KMIP_CLIENT;
metadata: {
id: string;
};
}
interface GetKmipClientEvent {
type: EventType.GET_KMIP_CLIENT;
metadata: {
id: string;
};
}
interface GetKmipClientsEvent {
type: EventType.GET_KMIP_CLIENTS;
metadata: {
ids: string[];
};
}
interface CreateKmipClientCertificateEvent {
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE;
metadata: {
clientId: string;
ttl: string;
keyAlgorithm: string;
serialNumber: string;
};
}
interface KmipOperationGetEvent {
type: EventType.KMIP_OPERATION_GET;
metadata: {
id: string;
};
}
interface KmipOperationDestroyEvent {
type: EventType.KMIP_OPERATION_DESTROY;
metadata: {
id: string;
};
}
interface KmipOperationCreateEvent {
type: EventType.KMIP_OPERATION_CREATE;
metadata: {
id: string;
algorithm: string;
};
}
interface KmipOperationGetAttributesEvent {
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES;
metadata: {
id: string;
};
}
interface KmipOperationActivateEvent {
type: EventType.KMIP_OPERATION_ACTIVATE;
metadata: {
id: string;
};
}
interface KmipOperationRevokeEvent {
type: EventType.KMIP_OPERATION_REVOKE;
metadata: {
id: string;
};
}
interface KmipOperationLocateEvent {
type: EventType.KMIP_OPERATION_LOCATE;
metadata: {
ids: string[];
};
}
interface KmipOperationRegisterEvent {
type: EventType.KMIP_OPERATION_REGISTER;
metadata: {
id: string;
algorithm: string;
name: string;
};
}
interface SetupKmipEvent {
type: EventType.SETUP_KMIP;
metadata: {
keyAlgorithm: CertKeyAlgorithm;
};
}
interface GetKmipEvent {
type: EventType.GET_KMIP;
metadata: {
id: string;
};
}
interface RegisterKmipServerEvent {
type: EventType.REGISTER_KMIP_SERVER;
metadata: {
serverCertificateSerialNumber: string;
hostnamesOrIps: string;
commonName: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -2275,4 +2453,21 @@ export type Event =
| SecretSyncImportSecretsEvent
| SecretSyncRemoveSecretsEvent
| OidcGroupMembershipMappingAssignUserEvent
| OidcGroupMembershipMappingRemoveUserEvent;
| OidcGroupMembershipMappingRemoveUserEvent
| CreateKmipClientEvent
| UpdateKmipClientEvent
| DeleteKmipClientEvent
| GetKmipClientEvent
| GetKmipClientsEvent
| CreateKmipClientCertificateEvent
| SetupKmipEvent
| GetKmipEvent
| RegisterKmipServerEvent
| KmipOperationGetEvent
| KmipOperationDestroyEvent
| KmipOperationCreateEvent
| KmipOperationGetAttributesEvent
| KmipOperationActivateEvent
| KmipOperationRevokeEvent
| KmipOperationLocateEvent
| KmipOperationRegisterEvent;

View File

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipClientCertificateDALFactory = ReturnType<typeof kmipClientCertificateDALFactory>;
export const kmipClientCertificateDALFactory = (db: TDbClient) => {
const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates);
return kmipClientCertOrm;
};

View File

@ -0,0 +1,86 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TKmipClients } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { KmipClientOrderBy } from "./kmip-types";
export type TKmipClientDALFactory = ReturnType<typeof kmipClientDALFactory>;
export const kmipClientDALFactory = (db: TDbClient) => {
const kmipClientOrm = ormify(db, TableName.KmipClient);
const findByProjectAndClientId = async (projectId: string, clientId: string) => {
try {
const client = await db
.replicaNode()(TableName.KmipClient)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.where({
[`${TableName.KmipClient}.projectId` as "projectId"]: projectId,
[`${TableName.KmipClient}.id` as "id"]: clientId
})
.select(selectAllTableCols(TableName.KmipClient))
.select(db.ref("id").withSchema(TableName.Organization).as("orgId"))
.first();
return client;
} catch (error) {
throw new DatabaseError({ error, name: "Find by project and client ID" });
}
};
const findByProjectId = async (
{
projectId,
offset = 0,
limit,
orderBy = KmipClientOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search
}: {
projectId: string;
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
},
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.KmipClient)
.where("projectId", projectId)
.where((qb) => {
if (search) {
void qb.whereILike("name", `%${search}%`);
}
})
.select<
(TKmipClients & {
total_count: number;
})[]
>(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`))
.orderBy(orderBy, orderDirection);
if (limit) {
void query.limit(limit).offset(offset);
}
const data = await query;
return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) };
} catch (error) {
throw new DatabaseError({ error, name: "Find KMIP clients by project id" });
}
};
return {
...kmipClientOrm,
findByProjectId,
findByProjectAndClientId
};
};

View File

@ -0,0 +1,11 @@
export enum KmipPermission {
Create = "create",
Locate = "locate",
Check = "check",
Get = "get",
GetAttributes = "get-attributes",
Activate = "activate",
Revoke = "revoke",
Destroy = "destroy",
Register = "register"
}

View File

@ -0,0 +1,422 @@
import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TKmipClientDALFactory } from "./kmip-client-dal";
import { KmipPermission } from "./kmip-enum";
import {
TKmipCreateDTO,
TKmipDestroyDTO,
TKmipGetAttributesDTO,
TKmipGetDTO,
TKmipLocateDTO,
TKmipRegisterDTO,
TKmipRevokeDTO
} from "./kmip-types";
type TKmipOperationServiceFactoryDep = {
kmsService: TKmsServiceFactory;
kmsDAL: TKmsKeyDALFactory;
kmipClientDAL: TKmipClientDALFactory;
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>;
export const kmipOperationServiceFactory = ({
kmsService,
kmsDAL,
projectDAL,
kmipClientDAL,
permissionService
}: TKmipOperationServiceFactoryDep) => {
const create = async ({
projectId,
clientId,
algorithm,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipCreateDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Create)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP create"
});
}
const kmsKey = await kmsService.generateKmsKey({
encryptionAlgorithm: algorithm,
orgId: actorOrgId,
projectId,
isReserved: false
});
return kmsKey;
};
const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Destroy)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP destroy"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot destroy reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot destroy external keys"
});
}
if (!completeKeyDetails.isDisabled) {
throw new BadRequestError({
message: "Cannot destroy active keys"
});
}
const kms = kmsDAL.deleteById(id);
return kms;
};
const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Get)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP get"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot get reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot get external keys"
});
}
const kmsKey = await kmsService.getKeyMaterial({
kmsId: key.id
});
return {
id: key.id,
value: kmsKey.toString("base64"),
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
isActive: !key.isDisabled,
createdAt: key.createdAt,
updatedAt: key.updatedAt
};
};
const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Activate)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP activate"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
return {
id: key.id,
isActive: !key.isDisabled
};
};
const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Revoke)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP revoke"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot revoke reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot revoke external keys"
});
}
const revokedKey = await kmsDAL.updateById(key.id, {
isDisabled: true
});
return {
id: key.id,
updatedAt: revokedKey.updatedAt
};
};
const getAttributes = async ({
projectId,
id,
clientId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipGetAttributesDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.GetAttributes)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP get attributes"
});
}
const key = await kmsDAL.findOne({
id,
projectId
});
if (!key) {
throw new NotFoundError({ message: `Key with ID ${id} not found` });
}
if (key.isReserved) {
throw new BadRequestError({ message: "Cannot get reserved keys" });
}
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
if (!completeKeyDetails.internalKms) {
throw new BadRequestError({
message: "Cannot get external keys"
});
}
return {
id: key.id,
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
isActive: !key.isDisabled,
createdAt: key.createdAt,
updatedAt: key.updatedAt
};
};
const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Locate)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP locate"
});
}
const keys = await kmsDAL.findProjectCmeks(projectId);
return keys;
};
const register = async ({
projectId,
clientId,
key,
algorithm,
name,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TKmipRegisterDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipClient = await kmipClientDAL.findOne({
id: clientId,
projectId
});
if (!kmipClient.permissions?.includes(KmipPermission.Register)) {
throw new ForbiddenRequestError({
message: "Client does not have sufficient permission to perform KMIP register"
});
}
const project = await projectDAL.findById(projectId);
const kmsKey = await kmsService.importKeyMaterial({
name,
key: Buffer.from(key, "base64"),
algorithm,
isReserved: false,
projectId,
orgId: project.orgId
});
return kmsKey;
};
return {
create,
get,
activate,
getAttributes,
destroy,
revoke,
locate,
register
};
};

View File

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipOrgConfigDALFactory = ReturnType<typeof kmipOrgConfigDALFactory>;
export const kmipOrgConfigDALFactory = (db: TDbClient) => {
const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig);
return kmipOrgConfigOrm;
};

View File

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmipOrgServerCertificateDALFactory = ReturnType<typeof kmipOrgServerCertificateDALFactory>;
export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => {
const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates);
return kmipOrgServerCertificateOrm;
};

View File

@ -0,0 +1,817 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { ActionProjectType } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidHostname, isValidIp } from "@app/lib/ip";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
createSerialNumber,
keyAlgorithmToAlgCfg
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
import { TKmipClientDALFactory } from "./kmip-client-dal";
import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal";
import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal";
import {
TCreateKmipClientCertificateDTO,
TCreateKmipClientDTO,
TDeleteKmipClientDTO,
TGenerateOrgKmipServerCertificateDTO,
TGetKmipClientDTO,
TGetOrgKmipDTO,
TListKmipClientsByProjectIdDTO,
TRegisterServerDTO,
TSetupOrgKmipDTO,
TUpdateKmipClientDTO
} from "./kmip-types";
type TKmipServiceFactoryDep = {
kmipClientDAL: TKmipClientDALFactory;
kmipClientCertificateDAL: TKmipClientCertificateDALFactory;
kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
kmipOrgConfigDAL: TKmipOrgConfigDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
export const kmipServiceFactory = ({
kmipClientDAL,
permissionService,
kmipClientCertificateDAL,
kmipOrgConfigDAL,
kmsService,
kmipOrgServerCertificateDAL,
licenseService
}: TKmipServiceFactoryDep) => {
const createKmipClient = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
name,
description,
permissions
}: TCreateKmipClientDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionSub.Kmip
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
});
const kmipClient = await kmipClientDAL.create({
projectId,
name,
description,
permissions
});
return kmipClient;
};
const updateKmipClient = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
name,
description,
permissions,
id
}: TUpdateKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to update KMIP client. Upgrade your plan to enterprise."
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionSub.Kmip
);
const updatedKmipClient = await kmipClientDAL.updateById(id, {
name,
description,
permissions
});
return updatedKmipClient;
};
const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionSub.Kmip
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to delete KMIP client. Upgrade your plan to enterprise."
});
const deletedKmipClient = await kmipClientDAL.deleteById(id);
return deletedKmipClient;
};
const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => {
const kmipClient = await kmipClientDAL.findById(id);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${id} does not exist`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
return kmipClient;
};
const listKmipClientsByProjectId = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
...rest
}: TListKmipClientsByProjectIdDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
return kmipClientDAL.findByProjectId({ projectId, ...rest });
};
const createKmipClientCertificate = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
ttl,
keyAlgorithm,
clientId
}: TCreateKmipClientCertificateDTO) => {
const kmipClient = await kmipClientDAL.findById(clientId);
if (!kmipClient) {
throw new NotFoundError({
message: `KMIP client with ID ${clientId} does not exist`
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
});
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: kmipClient.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.KMS
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionKmipActions.GenerateClientCertificates,
ProjectPermissionSub.Kmip
);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (!kmipConfig) {
throw new InternalServerError({
message: "KMIP has not been configured for the organization"
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const caCertObj = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
];
const caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm);
const caSkObj = crypto.createPrivateKey({
key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }),
format: "der",
type: "pkcs8"
});
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
caSkObj.export({ format: "der", type: "pkcs8" }),
caAlg,
true,
["sign"]
);
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `OU=${kmipClient.projectId},CN=${clientId}`,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: leafKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
const serverIntermediateCaCert = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
);
await kmipClientCertificateDAL.create({
kmipClientId: clientId,
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
serialNumber
});
return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
projectId: kmipClient.projectId
};
};
const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => {
const serverCert = await kmipOrgServerCertificateDAL.findOne({
serialNumber,
orgId
});
if (!serverCert) {
throw new NotFoundError({
message: "Server certificate not found"
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate }));
return {
publicKey: parsedCertificate.publicKey.toString("pem"),
keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm
};
};
const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (kmipConfig) {
throw new BadRequestError({
message: "KMIP has already been configured for the organization"
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to setup KMIP. Upgrade your plan to enterprise."
});
const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm);
// generate root CA
const rootCaSerialNumber = createSerialNumber();
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey);
const rootCaIssuedAt = new Date();
const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20));
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
name: `CN=KMIP Root CA,OU=${actorOrgId}`,
serialNumber: rootCaSerialNumber,
notBefore: rootCaIssuedAt,
notAfter: rootCaExpiration,
signingAlgorithm: alg,
keys: rootCaKeys,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
]
});
// generate intermediate server CA
const serverIntermediateCaSerialNumber = createSerialNumber();
const serverIntermediateCaIssuedAt = new Date();
const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey);
const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({
serialNumber: serverIntermediateCaSerialNumber,
subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`,
issuer: rootCaCert.subject,
notBefore: serverIntermediateCaIssuedAt,
notAfter: serverIntermediateCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: serverIntermediateCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(serverIntermediateCaKeys.publicKey)
]
});
// generate intermediate client CA
const clientIntermediateCaSerialNumber = createSerialNumber();
const clientIntermediateCaIssuedAt = new Date();
const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey);
const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({
serialNumber: clientIntermediateCaSerialNumber,
subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`,
issuer: rootCaCert.subject,
notBefore: clientIntermediateCaIssuedAt,
notAfter: clientIntermediateCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: clientIntermediateCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(clientIntermediateCaKeys.publicKey)
]
});
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
await kmipOrgConfigDAL.create({
orgId: actorOrgId,
caKeyAlgorithm,
rootCaIssuedAt,
rootCaExpiration,
rootCaSerialNumber,
encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
encryptedRootCaPrivateKey: encryptor({
plainText: rootCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
serverIntermediateCaIssuedAt,
serverIntermediateCaExpiration,
serverIntermediateCaSerialNumber,
encryptedServerIntermediateCaCertificate: encryptor({
plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData))
}).cipherTextBlob,
encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
.cipherTextBlob,
encryptedServerIntermediateCaPrivateKey: encryptor({
plainText: serverIntermediateCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
clientIntermediateCaIssuedAt,
clientIntermediateCaExpiration,
clientIntermediateCaSerialNumber,
encryptedClientIntermediateCaCertificate: encryptor({
plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData))
}).cipherTextBlob,
encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
.cipherTextBlob,
encryptedClientIntermediateCaPrivateKey: encryptor({
plainText: clientIntermediateCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob
});
return {
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
};
};
const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => {
await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (!kmipConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the organization"
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
const serverIntermediateCaCert = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
);
const clientIntermediateCaCert = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
);
return {
id: kmipConfig.id,
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
};
};
const generateOrgKmipServerCertificate = async ({
orgId,
ttl,
commonName,
altNames,
keyAlgorithm
}: TGenerateOrgKmipServerCertificateDTO) => {
const kmipOrgConfig = await kmipOrgConfigDAL.findOne({
orgId
});
if (!kmipOrgConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the organization"
});
}
const plan = await licenseService.getPlan(orgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise."
});
const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const caCertObj = new x509.X509Certificate(
decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate })
);
const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true)
];
const altNamesArray: {
type: "email" | "dns" | "ip";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
if (isValidHostname(altName)) {
return {
type: "dns",
value: altName
};
}
if (isValidIp(altName)) {
return {
type: "ip",
value: altName
};
}
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm);
const decryptedCaCertChain = decryptor({
cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain
}).toString("utf-8");
const caSkObj = crypto.createPrivateKey({
key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }),
format: "der",
type: "pkcs8"
});
const caPrivateKey = await crypto.subtle.importKey(
"pkcs8",
caSkObj.export({ format: "der", type: "pkcs8" }),
caAlg,
true,
["sign"]
);
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `CN=${commonName}`,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: leafKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
await kmipOrgServerCertificateDAL.create({
orgId,
keyAlgorithm,
issuedAt: notBeforeDate,
expiration: notAfterDate,
serialNumber,
commonName,
altNames,
encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob,
encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob
});
return {
serialNumber,
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
certificate: leafCert.toString("pem"),
certificateChain
};
};
const registerServer = async ({
actorOrgId,
actor,
actorId,
actorAuthMethod,
ttl,
commonName,
keyAlgorithm,
hostnamesOrIps
}: TRegisterServerDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId
});
if (!kmipConfig) {
throw new BadRequestError({
message: "KMIP has not been configured for the organization"
});
}
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.kmip)
throw new BadRequestError({
message: "Failed to register KMIP server. Upgrade your plan to enterprise."
});
const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({
orgId: actorOrgId,
commonName: commonName ?? "kmip-server",
altNames: hostnamesOrIps,
keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm),
ttl
});
const { clientCertificateChain } = await getOrgKmip({
actor,
actorAuthMethod,
actorId,
actorOrgId
});
return {
serverCertificateSerialNumber: serialNumber,
clientCertificateChain,
privateKey,
certificate,
certificateChain
};
};
return {
createKmipClient,
updateKmipClient,
deleteKmipClient,
getKmipClient,
listKmipClientsByProjectId,
createKmipClientCertificate,
setupOrgKmip,
generateOrgKmipServerCertificate,
getOrgKmip,
getServerCertificateBySerialNumber,
registerServer
};
};

View File

@ -0,0 +1,102 @@
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { KmipPermission } from "./kmip-enum";
export type TCreateKmipClientCertificateDTO = {
clientId: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateKmipClientDTO = {
name: string;
description?: string;
permissions: KmipPermission[];
} & TProjectPermission;
export type TUpdateKmipClientDTO = {
id: string;
name?: string;
description?: string;
permissions?: KmipPermission[];
} & Omit<TProjectPermission, "projectId">;
export type TDeleteKmipClientDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetKmipClientDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export enum KmipClientOrderBy {
Name = "name"
}
export type TListKmipClientsByProjectIdDTO = {
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & TProjectPermission;
type KmipOperationBaseDTO = {
clientId: string;
projectId: string;
} & Omit<TOrgPermission, "orgId">;
export type TKmipCreateDTO = {
algorithm: SymmetricEncryption;
} & KmipOperationBaseDTO;
export type TKmipGetDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipGetAttributesDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipDestroyDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipActivateDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipRevokeDTO = {
id: string;
} & KmipOperationBaseDTO;
export type TKmipLocateDTO = KmipOperationBaseDTO;
export type TKmipRegisterDTO = {
name: string;
key: string;
algorithm: SymmetricEncryption;
} & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
} & Omit<TOrgPermission, "orgId">;
export type TGetOrgKmipDTO = Omit<TOrgPermission, "orgId">;
export type TGenerateOrgKmipServerCertificateDTO = {
commonName: string;
altNames: string;
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
orgId: string;
};
export type TRegisterServerDTO = {
hostnamesOrIps: string;
commonName?: string;
keyAlgorithm?: CertKeyAlgorithm;
ttl: string;
} & Omit<TOrgPermission, "orgId">;

View File

@ -50,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
},
pkiEst: false,
enforceMfa: false,
projectTemplates: false
projectTemplates: false,
kmip: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@ -68,6 +68,7 @@ export type TFeatureSet = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: false;
kmip: false;
};
export type TOrgPlansTableDTO = {

View File

@ -23,6 +23,11 @@ export enum OrgPermissionAppConnectionActions {
Connect = "connect"
}
export enum OrgPermissionKmipActions {
Proxy = "proxy",
Setup = "setup"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
@ -44,7 +49,8 @@ export enum OrgPermissionSubjects {
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
AppConnections = "app-connections",
Kmip = "kmip"
}
export type AppConnectionSubjectFields = {
@ -74,7 +80,8 @@ export type OrgPermissionSet =
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
)
]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
const AppConnectionConditionSchema = z
.object({
@ -167,6 +174,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
"Describe what action an entity can take."
)
})
]);
@ -253,6 +266,11 @@ const buildAdminPermission = () => {
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
return rules;
};

View File

@ -44,6 +44,14 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
DeleteClients = "delete-clients",
ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
}
export enum ProjectPermissionSub {
Role = "role",
Member = "member",
@ -75,7 +83,8 @@ export enum ProjectPermissionSub {
PkiCollections = "pki-collections",
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs"
SecretSyncs = "secret-syncs",
Kmip = "kmip"
}
export type SecretSubjectFields = {
@ -156,6 +165,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
@ -410,6 +420,12 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
"Describe what action an entity can take."
)
})
];
@ -575,6 +591,18 @@ const buildAdminPermissionRules = () => {
],
ProjectPermissionSub.SecretSyncs
);
can(
[
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
return rules;
};

View File

@ -721,7 +721,8 @@ export const RAW_SECRETS = {
secretName: "The name of the secret to update.",
secretComment: "Update comment to the secret.",
environment: "The slug of the environment where the secret is located.",
secretPath: "The path of the secret to update.",
mode: "Defines how the system should handle missing secrets during an update.",
secretPath: "The default path for secrets to update or upsert, if not provided in the secret details.",
secretValue: "The new value of the secret.",
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
type: "The type of the secret to update.",

View File

@ -103,6 +103,16 @@ export const isValidIpOrCidr = (ip: string): boolean => {
return false;
};
export const isValidIp = (ip: string) => {
return net.isIPv4(ip) || net.isIPv6(ip);
};
export const isValidHostname = (name: string) => {
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
return hostnameRegex.test(name);
};
export type TIp = {
ipAddress: string;
type: IPType;

View File

@ -35,6 +35,12 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal";
import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal";
import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
@ -382,6 +388,10 @@ export const registerRoutes = async (
const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const kmipClientDAL = kmipClientDALFactory(db);
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@ -1429,6 +1439,24 @@ export const registerRoutes = async (
keyStore
});
const kmipService = kmipServiceFactory({
kmipClientDAL,
permissionService,
kmipClientCertificateDAL,
kmipOrgConfigDAL,
kmsService,
kmipOrgServerCertificateDAL,
licenseService
});
const kmipOperationService = kmipOperationServiceFactory({
kmsService,
kmsDAL,
projectDAL,
kmipClientDAL,
permissionService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@ -1527,7 +1555,9 @@ export const registerRoutes = async (
projectTemplate: projectTemplateService,
totp: totpService,
appConnection: appConnectionService,
secretSync: secretSyncService
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService
});
const cronJobs: CronJob[] = [];
@ -1539,7 +1569,8 @@ export const registerRoutes = async (
}
server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL
user: userDAL,
kmipClient: kmipClientDAL
});
await server.register(injectIdentity, { userDAL, serviceTokenDAL });

View File

@ -2,10 +2,9 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
server.route({
@ -21,18 +20,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req, res) => {
const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
if (req.auth.authMode === AuthMode.JWT) {
await server.services.login.logout(req.permission.id, req.auth.tokenVersionId);
}
await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId);
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully logged out" };
}
});
@ -69,37 +69,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const refreshToken = req.cookies.jid;
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
if (!refreshToken)
throw new NotFoundError({
name: "AuthTokenNotFound",
message: "Failed to find refresh token"
});
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
throw new UnauthorizedError({
message: "The token provided is not a refresh token",
name: "InvalidToken"
});
const tokenVersion = await server.services.authToken.getUserTokenSessionById(
decodedToken.tokenVersionId,
decodedToken.userId
);
if (!tokenVersion)
throw new UnauthorizedError({
message: "Valid token version not found",
name: "InvalidToken"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
throw new UnauthorizedError({
message: "Token version mismatch",
name: "InvalidToken"
});
}
const token = jwt.sign(
{

View File

@ -20,6 +20,7 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas";
@ -2030,6 +2031,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.UPDATE.secretPath),
mode: z
.nativeEnum(SecretUpdateMode)
.optional()
.default(SecretUpdateMode.FailOnNotFound)
.describe(RAW_SECRETS.UPDATE.mode),
secrets: z
.object({
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
@ -2037,6 +2043,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.UPDATE.secretValue),
secretPath: z
.string()
.trim()
.transform(removeTrailingSlash)
.optional()
.describe(RAW_SECRETS.UPDATE.secretPath),
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
@ -2073,7 +2085,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment,
projectSlug,
projectId: req.body.workspaceId,
secrets: inputSecrets
secrets: inputSecrets,
mode: req.body.mode
});
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
@ -2092,15 +2105,39 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: secrets.map((secret) => ({
secretId: secret.id,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
}))
secrets: secrets
.filter((el) => el.version > 1)
.map((secret) => ({
secretId: secret.id,
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
}))
}
}
});
const createdSecrets = secrets.filter((el) => el.version === 1);
if (createdSecrets.length) {
await server.services.auditLog.createAuditLog({
projectId: secrets[0].workspace,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRETS,
metadata: {
environment: req.body.environment,
secretPath: req.body.secretPath,
secrets: createdSecrets.map((secret) => ({
secretId: secret.id,
secretPath: secret.secretPath,
secretKey: secret.secretKey,
secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey)
}))
}
}
});
}
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretUpdated,

View File

@ -1,6 +1,7 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Knex } from "knex";
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
@ -8,7 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
@ -150,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
const validateRefreshToken = async (refreshToken?: string) => {
const appCfg = getConfig();
if (!refreshToken)
throw new NotFoundError({
name: "AuthTokenNotFound",
message: "Failed to find refresh token"
});
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
throw new UnauthorizedError({
message: "The token provided is not a refresh token",
name: "InvalidToken"
});
const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId);
if (!tokenVersion)
throw new UnauthorizedError({
message: "Valid token version not found",
name: "InvalidToken"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
throw new UnauthorizedError({
message: "Token version mismatch",
name: "InvalidToken"
});
}
return { decodedToken, tokenVersion };
};
// to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const session = await tokenDAL.findOneTokenSession({
@ -188,6 +223,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
clearTokenSessionById,
getTokenSessionByUser,
revokeAllMySessions,
validateRefreshToken,
fnValidateJwtIdentity,
getUserTokenSessionById
};

View File

@ -35,6 +35,7 @@ export enum AuthMode {
export enum ActorType { // would extend to AWS, Azure, ...
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
KMIP_CLIENT = "kmipClient",
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity",

View File

@ -1,5 +1,7 @@
import { z } from "zod";
import { isValidIp } from "@app/lib/ip";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
@ -25,7 +27,7 @@ export const validateAltNamesField = z
if (data === "") return true;
// Split and validate each alt name
return data.split(", ").every((name) => {
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
});
},
{

View File

@ -40,3 +40,9 @@ export const isCertChainValid = async (certificates: x509.X509Certificate[]) =>
// chain.build() implicitly verifies the chain
return chainItems.length === certificates.length;
};
export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) =>
certificates
.map((cert) => cert.toString("pem"))
.join("\n")
.trim();

View File

@ -93,6 +93,32 @@ export const kmskeyDALFactory = (db: TDbClient) => {
}
};
const findProjectCmeks = async (projectId: string, tx?: Knex) => {
try {
const result = await (tx || db.replicaNode())(TableName.KmsKey)
.where({
[`${TableName.KmsKey}.projectId` as "projectId"]: projectId,
[`${TableName.KmsKey}.isReserved` as "isReserved"]: false
})
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
.join(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
.select(selectAllTableCols(TableName.KmsKey))
.select(
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"),
db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion")
);
return result.map((entry) => ({
...KmsKeysSchema.parse(entry),
isActive: !entry.isDisabled,
algorithm: entry.internalKmsEncryptionAlgorithm,
version: entry.internalKmsVersion
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find project cmeks" });
}
};
const listCmeksByProjectId = async (
{
projectId,
@ -167,5 +193,5 @@ export const kmskeyDALFactory = (db: TDbClient) => {
}
};
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName };
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks };
};

View File

@ -1,8 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { Knex } from "knex";
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;

View File

@ -37,6 +37,8 @@ import {
TEncryptWithKmsDataKeyDTO,
TEncryptWithKmsDTO,
TGenerateKMSDTO,
TGetKeyMaterialDTO,
TImportKeyMaterialDTO,
TUpdateProjectSecretManagerKmsKeyDTO
} from "./kms-types";
@ -325,6 +327,72 @@ export const kmsServiceFactory = ({
};
};
const getKeyMaterial = async ({ kmsId }: TGetKeyMaterialDTO) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
if (!kmsDoc) {
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
}
if (kmsDoc.isReserved) {
throw new BadRequestError({
message: "Cannot get key material for reserved key"
});
}
if (kmsDoc.externalKms) {
throw new BadRequestError({
message: "Cannot get key material for external key"
});
}
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
return kmsKey;
};
const importKeyMaterial = async (
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
tx?: Knex
) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
if (key.byteLength !== expectedByteLength) {
throw new BadRequestError({
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
});
}
const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY);
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => {
const kmsDoc = await kmsDAL.create(
{
name: sanitizedName,
orgId,
isReserved,
projectId
},
db
);
await internalKmsDAL.create(
{
version: 1,
encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: algorithm,
kmsKeyId: kmsDoc.id
},
db
);
return kmsDoc;
};
if (tx) return dbQuery(tx);
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
return doc;
};
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
if (!kmsDoc) {
@ -944,6 +1012,8 @@ export const kmsServiceFactory = ({
getProjectKeyBackup,
loadProjectKeyBackup,
getKmsById,
createCipherPairWithDataKey
createCipherPairWithDataKey,
getKeyMaterial,
importKeyMaterial
};
};

View File

@ -61,3 +61,15 @@ export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
export type TGetKeyMaterialDTO = {
kmsId: string;
};
export type TImportKeyMaterialDTO = {
key: Buffer;
algorithm: SymmetricEncryption;
name?: string;
isReserved: boolean;
projectId: string;
orgId: string;
};

View File

@ -272,22 +272,6 @@ export const AwsSecretsManagerSyncFns = {
SecretString: secretValue
});
}
for await (const secretKey of Object.keys(awsSecretsRecord)) {
if (secretKey === destinationConfig.secretName) {
// eslint-disable-next-line no-continue
continue;
}
try {
await deleteSecret(client, secretKey);
} catch (error) {
throw new SecretSyncError({
error,
secretKey
});
}
}
}
},
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {

View File

@ -1,7 +1,15 @@
import { ForbiddenError, PureAbility, subject } from "@casl/ability";
import { Knex } from "knex";
import { z } from "zod";
import { ActionProjectType, ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
import {
ActionProjectType,
ProjectMembershipRole,
SecretsV2Schema,
SecretType,
TableName,
TSecretsV2
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -36,6 +44,7 @@ import {
} from "./secret-v2-bridge-fns";
import {
SecretOperations,
SecretUpdateMode,
TBackFillSecretReferencesDTO,
TCreateManySecretDTO,
TCreateSecretDTO,
@ -103,12 +112,13 @@ export const secretV2BridgeServiceFactory = ({
const $validateSecretReferences = async (
projectId: string,
permission: PureAbility,
references: ReturnType<typeof getAllSecretReferences>["nestedReferences"]
references: ReturnType<typeof getAllSecretReferences>["nestedReferences"],
tx?: Knex
) => {
if (!references.length) return;
const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment)));
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs);
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx);
if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length)
throw new BadRequestError({
message: `Referenced environment not found. Missing ${diff(
@ -122,36 +132,41 @@ export const secretV2BridgeServiceFactory = ({
references.map((el) => ({
secretPath: el.secretPath,
envId: referencesEnvironmentGroupBySlug[el.environment][0].id
}))
})),
tx
);
const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`);
const referredSecrets = await secretDAL.find({
$complex: {
operator: "or",
value: references.map((el) => {
const folderId =
referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`][0]
?.id;
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
const referredSecrets = await secretDAL.find(
{
$complex: {
operator: "or",
value: references.map((el) => {
const folderId =
referencesFolderGroupByPath[
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
][0]?.id;
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
return {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: folderId
},
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
};
})
}
});
return {
operator: "and",
value: [
{
operator: "eq",
field: "folderId",
value: folderId
},
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
};
})
}
},
{ tx }
);
if (
referredSecrets.length !==
@ -1245,8 +1260,9 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
environment,
projectId,
secretPath,
secrets: inputSecrets
secretPath: defaultSecretPath = "/",
secrets: inputSecrets,
mode: updateMode
}: TUpdateManySecretDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
@ -1257,196 +1273,280 @@ export const secretV2BridgeServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager
});
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
const secretsToUpdateGroupByPath = groupBy(inputSecrets, (el) => el.secretPath || defaultSecretPath);
const projectEnvironment = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!projectEnvironment) {
throw new NotFoundError({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`
});
}
const folders = await folderDAL.findByManySecretPath(
Object.keys(secretsToUpdateGroupByPath).map((el) => ({ envId: projectEnvironment.id, secretPath: el }))
);
if (folders.length !== Object.keys(secretsToUpdateGroupByPath).length)
throw new NotFoundError({
message: `Folder with path '${null}' in environment with slug '${environment}' not found`,
name: "UpdateManySecret"
});
const folderId = folder.id;
const secretsToUpdate = await secretDAL.find({
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: inputSecrets.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
});
if (secretsToUpdate.length !== inputSecrets.length) {
const secretsToUpdateNames = secretsToUpdate.map((secret) => secret.key);
const invalidSecrets = inputSecrets.filter((secret) => !secretsToUpdateNames.includes(secret.secretKey));
throw new NotFoundError({
message: `Secret does not exist: ${invalidSecrets.map((el) => el.secretKey).join(",")}`
});
}
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key);
secretsToUpdate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
});
// get all tags
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupByID = groupBy(tags, (i) => i.id);
// check again to avoid non authorized tags are removed
inputSecrets.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.secretKey,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
// now find any secret that needs to update its name
// same process as above
const secretsWithNewName = inputSecrets.filter(({ newSecretName }) => Boolean(newSecretName));
if (secretsWithNewName.length) {
const secrets = await secretDAL.find({
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: secretsWithNewName.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
});
if (secrets.length)
throw new BadRequestError({
message: `Secret with new name already exists: ${secretsWithNewName.map((el) => el.newSecretName).join(",")}`
});
secretsWithNewName.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.newSecretName as string,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
}
// now get all secret references made and validate the permission
const secretReferencesGroupByInputSecretKey: Record<string, ReturnType<typeof getAllSecretReferences>> = {};
const secretReferences: TSecretReference[] = [];
inputSecrets.forEach((el) => {
if (el.secretValue) {
const references = getAllSecretReferences(el.secretValue);
secretReferencesGroupByInputSecretKey[el.secretKey] = references;
secretReferences.push(...references.nestedReferences);
references.localReferences.forEach((localRefKey) => {
secretReferences.push({ secretKey: localRefKey, secretPath, environment });
});
}
});
await $validateSecretReferences(projectId, permission, secretReferences);
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: inputSecrets.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
const encryptedValue =
typeof el.secretValue !== "undefined"
? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
}
: {};
const updatedSecrets: Array<TSecretsV2 & { secretPath: string }> = [];
await secretDAL.transaction(async (tx) => {
for await (const folder of folders) {
if (!folder) throw new NotFoundError({ message: "Folder not found" });
return {
filter: { id: originalSecret.id, type: SecretType.Shared },
data: {
reminderRepeatDays: el.secretReminderRepeatDays,
encryptedComment: setKnexStringValue(
el.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
reminderNote: el.secretReminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey,
tags: el.tagIds,
secretMetadata: el.secretMetadata,
...encryptedValue
const folderId = folder.id;
const secretPath = folder.path;
let secretsToUpdate = secretsToUpdateGroupByPath[secretPath];
const secretsToUpdateInDB = await secretDAL.find(
{
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: secretsToUpdate.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
};
}),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
resourceMetadataDAL
})
);
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
actor,
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
},
{ tx }
);
if (secretsToUpdateInDB.length !== secretsToUpdate.length && updateMode === SecretUpdateMode.FailOnNotFound)
throw new NotFoundError({
message: `Secret does not exist: ${diff(
secretsToUpdate.map((el) => el.secretKey),
secretsToUpdateInDB.map((el) => el.key)
).join(", ")} in path ${folder.path}`
});
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdateInDB, (i) => i.key);
const secretsToCreate = secretsToUpdate.filter((el) => !secretsToUpdateInDBGroupedByKey?.[el.secretKey]);
secretsToUpdate = secretsToUpdate.filter((el) => secretsToUpdateInDBGroupedByKey?.[el.secretKey]);
secretsToUpdateInDB.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.key,
secretTags: el.tags.map((i) => i.slug)
})
);
});
// get all tags
const sanitizedTagIds = secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds, tx) : [];
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
const tagsGroupByID = groupBy(tags, (i) => i.id);
// check create permission allowed in upsert mode
if (updateMode === SecretUpdateMode.Upsert) {
secretsToCreate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.secretKey,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
}
// check again to avoid non authorized tags are removed
secretsToUpdate.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.secretKey,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
// now find any secret that needs to update its name
// same process as above
const secretsWithNewName = secretsToUpdate.filter(({ newSecretName }) => Boolean(newSecretName));
if (secretsWithNewName.length) {
const secrets = await secretDAL.find(
{
folderId,
$complex: {
operator: "and",
value: [
{
operator: "or",
value: secretsWithNewName.map((el) => ({
operator: "and",
value: [
{
operator: "eq",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
operator: "eq",
field: "type",
value: SecretType.Shared
}
]
}))
}
]
}
},
{ tx }
);
if (secrets.length)
throw new BadRequestError({
message: `Secret with new name already exists: ${secretsWithNewName
.map((el) => el.newSecretName)
.join(", ")}`
});
secretsWithNewName.forEach((el) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: el.newSecretName as string,
secretTags: (el.tagIds || []).map((i) => tagsGroupByID[i][0].slug)
})
);
});
}
// now get all secret references made and validate the permission
const secretReferencesGroupByInputSecretKey: Record<string, ReturnType<typeof getAllSecretReferences>> = {};
const secretReferences: TSecretReference[] = [];
secretsToUpdate.concat(SecretUpdateMode.Upsert === updateMode ? secretsToCreate : []).forEach((el) => {
if (el.secretValue) {
const references = getAllSecretReferences(el.secretValue);
secretReferencesGroupByInputSecretKey[el.secretKey] = references;
secretReferences.push(...references.nestedReferences);
references.localReferences.forEach((localRefKey) => {
secretReferences.push({ secretKey: localRefKey, secretPath, environment });
});
}
});
await $validateSecretReferences(projectId, permission, secretReferences, tx);
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: secretsToUpdate.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
const encryptedValue =
typeof el.secretValue !== "undefined"
? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
}
: {};
return {
filter: { id: originalSecret.id, type: SecretType.Shared },
data: {
reminderRepeatDays: el.secretReminderRepeatDays,
encryptedComment: setKnexStringValue(
el.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
reminderNote: el.secretReminderNote,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey,
tags: el.tagIds,
secretMetadata: el.secretMetadata,
...encryptedValue
}
};
}),
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
resourceMetadataDAL
});
updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
if (updateMode === SecretUpdateMode.Upsert) {
const bulkInsertedSecrets = await fnSecretBulkInsert({
inputSecrets: secretsToCreate.map((el) => {
const references = secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences;
return {
version: 1,
encryptedComment: setKnexStringValue(
el.secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
),
encryptedValue: el.secretValue
? secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
: undefined,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.secretKey,
tagIds: el.tagIds,
references,
secretMetadata: el.secretMetadata,
type: SecretType.Shared
};
}),
folderId,
orgId: actorOrgId,
secretDAL,
resourceMetadataDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
});
updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path })));
}
}
});
return secrets.map((el) =>
reshapeBridgeSecret(projectId, environment, secretPath, {
await Promise.allSettled(folders.map((el) => (el?.id ? snapshotService.performSnapshot(el.id) : undefined)));
await Promise.allSettled(
folders.map((el) =>
el
? secretQueueService.syncSecrets({
actor,
actorId,
secretPath: el.path,
projectId,
orgId: actorOrgId,
environmentSlug: environment
})
: undefined
)
);
return updatedSecrets.map((el) =>
reshapeBridgeSecret(projectId, environment, el.secretPath, {
...el,
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""

View File

@ -23,6 +23,12 @@ export type TSecretReferenceDTO = {
secretKey: string;
};
export enum SecretUpdateMode {
Ignore = "ignore",
Upsert = "upsert",
FailOnNotFound = "failOnNotFound"
}
export type TGetSecretsDTO = {
expandSecretReferences?: boolean;
path: string;
@ -113,6 +119,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretPath: string;
projectId: string;
environment: string;
mode: SecretUpdateMode;
secrets: {
secretKey: string;
newSecretName?: string;
@ -123,6 +130,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
secretPath?: string;
}[];
};

View File

@ -30,7 +30,10 @@ import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { OrgServiceActor } from "@app/lib/types";
import { TGetSecretsRawByFolderMappingsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import {
SecretUpdateMode,
TGetSecretsRawByFolderMappingsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal";
@ -2012,6 +2015,7 @@ export const secretServiceFactory = ({
actorOrgId,
actorAuthMethod,
secretPath,
mode = SecretUpdateMode.FailOnNotFound,
secrets: inputSecrets = []
}: TUpdateManySecretRawDTO) => {
if (!projectSlug && !optionalProjectId)
@ -2076,7 +2080,8 @@ export const secretServiceFactory = ({
actorOrgId,
actor,
actorId,
secrets: inputSecrets
secrets: inputSecrets,
mode
});
return { type: SecretProtectionType.Direct as const, secrets };
}

View File

@ -17,6 +17,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
@ -274,6 +275,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
projectId?: string;
projectSlug?: string;
environment: string;
mode: SecretUpdateMode;
secrets: {
secretKey: string;
newSecretName?: string;

View File

@ -10,7 +10,8 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.4.7
github.com/infisical/go-sdk v0.4.8
github.com/infisical/infisical-kmip v0.3.5
github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
@ -65,6 +66,9 @@ require (
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/gosimple/slug v1.15.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
@ -77,6 +81,7 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
@ -91,12 +96,12 @@ require (
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
@ -108,7 +113,7 @@ require (
require (
github.com/fatih/color v1.17.0
github.com/go-resty/resty/v2 v2.13.1
github.com/go-resty/resty/v2 v2.16.5
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/manifoldco/promptui v0.9.0

View File

@ -152,8 +152,8 @@ github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02E
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -237,6 +237,10 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
@ -255,6 +259,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@ -265,8 +271,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.4.7 h1:+cxIdDfciMh0Syxbxbqjhvz9/ShnN1equ2zqlVQYGtw=
github.com/infisical/go-sdk v0.4.7/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
github.com/infisical/go-sdk v0.4.8 h1:aphRnaauC5//PkP1ZbY9RSK2RiT1LjPS5o4CbX0x5OQ=
github.com/infisical/go-sdk v0.4.8/go.mod h1:bMO9xSaBeXkDBhTIM4FkkREAfw2V8mv5Bm7lvo4+uDk=
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -340,6 +348,7 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -413,7 +422,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@ -448,11 +456,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -490,8 +495,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -530,13 +533,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -562,8 +560,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -612,22 +608,11 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -639,17 +624,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -702,8 +683,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

103
cli/packages/cmd/kmip.go Normal file
View File

@ -0,0 +1,103 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"fmt"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/util"
kmip "github.com/infisical/infisical-kmip"
"github.com/spf13/cobra"
)
var kmipCmd = &cobra.Command{
Example: `infisical kmip`,
Short: "Used to manage KMIP servers",
Use: "kmip",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
}
var kmipStartCmd = &cobra.Command{
Example: `infisical kmip start`,
Short: "Used to start a KMIP server",
Use: "start",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: startKmipServer,
}
func startKmipServer(cmd *cobra.Command, args []string) {
listenAddr, err := cmd.Flags().GetString("listen-address")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
identityAuthMethod, err := cmd.Flags().GetString("identity-auth-method")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
authMethodValid, strategy := util.IsAuthMethodValid(identityAuthMethod, false)
if !authMethodValid {
util.PrintErrorMessageAndExit(fmt.Sprintf("Invalid login method: %s", identityAuthMethod))
}
var identityClientId string
var identityClientSecret string
if strategy == util.AuthStrategy.UNIVERSAL_AUTH {
identityClientId, err = util.GetCmdFlagOrEnv(cmd, "identity-client-id", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME)
if err != nil {
util.HandleError(err, "Unable to parse identity client ID")
}
identityClientSecret, err = util.GetCmdFlagOrEnv(cmd, "identity-client-secret", util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME)
if err != nil {
util.HandleError(err, "Unable to parse identity client secret")
}
} else {
util.PrintErrorMessageAndExit(fmt.Sprintf("Unsupported login method: %s", identityAuthMethod))
}
serverName, err := cmd.Flags().GetString("server-name")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
certificateTTL, err := cmd.Flags().GetString("certificate-ttl")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
hostnamesOrIps, err := cmd.Flags().GetString("hostnames-or-ips")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
kmip.StartServer(kmip.ServerConfig{
Addr: listenAddr,
InfisicalBaseAPIURL: config.INFISICAL_URL,
IdentityClientId: identityClientId,
IdentityClientSecret: identityClientSecret,
ServerName: serverName,
CertificateTTL: certificateTTL,
HostnamesOrIps: hostnamesOrIps,
})
}
func init() {
kmipStartCmd.Flags().String("listen-address", "localhost:5696", "The address for the KMIP server to listen on. Defaults to localhost:5696")
kmipStartCmd.Flags().String("identity-auth-method", string(util.AuthStrategy.UNIVERSAL_AUTH), "The auth method to use for authenticating the machine identity. Defaults to universal-auth.")
kmipStartCmd.Flags().String("identity-client-id", "", "Universal auth client ID of machine identity")
kmipStartCmd.Flags().String("identity-client-secret", "", "Universal auth client secret of machine identity")
kmipStartCmd.Flags().String("server-name", "kmip-server", "The name of the KMIP server")
kmipStartCmd.Flags().String("certificate-ttl", "1y", "The TTL duration for the server certificate")
kmipStartCmd.Flags().String("hostnames-or-ips", "", "Comma-separated list of hostnames or IPs")
kmipCmd.AddCommand(kmipStartCmd)
rootCmd.AddCommand(kmipCmd)
}

View File

@ -4,9 +4,6 @@ title: "Backend development guide"
Suppose you're interested in implementing a new feature in Infisical's backend, let's call it "feature-x." Here are the general steps you should follow.
## Database schema migration
In order to run [schema migrations](https://en.wikipedia.org/wiki/Schema_migration#:~:text=A%20schema%20migration%20is%20performed,some%20newer%20or%20older%20version) you need to expose your database connection string. Create a `.env.migration` file to set the database connection URI for migration scripts, or alternatively, export the `DB_CONNECTION_URI` environment variable.
## Creating new database model
If your feature involves a change in the database, you need to first address this by generating the necessary database schemas.

View File

@ -0,0 +1,87 @@
---
title: "Terraform Cloud"
description: "How to authenticate with Infisical from Terraform Cloud using OIDC."
---
This guide will walk you through setting up Terraform Cloud to inject a [workload identity token](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens) and use it for OIDC-based authentication with the Infisical Terraform provider. You'll start by creating a machine identity in Infisical, then configure Terraform Cloud to pass the injected token into your Terraform runs.
<Steps>
<Step title="Create a Machine Identity in Infisical">
Follow the instructions [in this documentation](/documentation/platform/identities/oidc-auth/general) to create a machine identity with OIDC auth. Infisical OIDC configuration values for Terraform Cloud:
1. Set the OIDC Discovery URL to https://app.terraform.io.
2. Set the Issuer to https://app.terraform.io.
3. Configure the Audience to match the value you will use for **TFC_WORKLOAD_IDENTITY_AUDIENCE** in Terraform Cloud for the next step.
To view all possible claims available from Terraform cloud, visit [HashiCorps documentation](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#token-structure).
</Step>
<Step title="Enable Workload Identity Token Injection in Terraform Cloud">
<Tabs>
<Tab title="Generate single token">
1. **Navigate to your workspace** in Terraform Cloud.
2. **Add a workspace variable** named `TFC_WORKLOAD_IDENTITY_AUDIENCE`:
- **Key**: `TFC_WORKLOAD_IDENTITY_AUDIENCE`
- **Value**: For example, `my-infisical-audience`
- **Category**: Environment
> **Important**:
> - The presence of `TFC_WORKLOAD_IDENTITY_AUDIENCE` is required for Terraform Cloud to inject a token.
> - If you are self-hosting HCP Terraform agents, ensure they are **v1.7.0 or above**.
Once set, Terraform Cloud will inject a workload identity token into the run environment as `TFC_WORKLOAD_IDENTITY_TOKEN`.
</Tab>
<Tab title="(Optional) Generate Multiple Tokens">
If you need multiple tokens (each with a different audience), create additional variables:
```
TFC_WORKLOAD_IDENTITY_AUDIENCE_[YOUR_TAG_HERE]
```
For example:
- `TFC_WORKLOAD_IDENTITY_AUDIENCE_INFISICAL`
- `TFC_WORKLOAD_IDENTITY_AUDIENCE_OTHER_SERVICE`
Terraform Cloud will then inject:
- `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL`
- `TFC_WORKLOAD_IDENTITY_TOKEN_OTHER_SERVICE`
> **Note**:
> - The `[YOUR_TAG_HERE]` can only contain letters, numbers, and underscores.
> - You **cannot** use the reserved keyword `TYPE`.
> - Generating multiple tokens requires **v1.12.0 or later** if you are self-hosting agents.
</Tab>
</Tabs>
<Warning>
If you are running on self-hosted HCP Terraform agents, you must use v1.7.0 or later to enable token injection. If you need to generate multiple tokens, you must use v1.12.0 or later.
</Warning>
</Step>
<Step title="Configure the Infisical Provider">
In your Terraform configuration, reference the injected token by name. For example:
```hcl
provider "infisical" {
host = "https://app.infisical.com"
auth = {
oidc = {
identity_id = "<identity-id>"
# This must match the environment variable Terraform injects:
token_environment_variable_name = "TFC_WORKLOAD_IDENTITY_TOKEN"
}
}
}
```
- **`host`**: Defaults to `https://app.infisical.com`. Override if using a self-hosted Infisical instance.
- **`identity_id`**: The OIDC identity ID from Infisical.
- **`token_environment_variable_name`**: Must match the injected variable name from Terraform Cloud. If using single token, use `TFC_WORKLOAD_IDENTITY_TOKEN`. If using multiple tokens, choose the one you want to use (e.g., `TFC_WORKLOAD_IDENTITY_TOKEN_INFISICAL`).
</Step>
<Step title="Validate Your Setup">
1. Run a plan and apply in Terraform Cloud.
2. Verify the Infisical provider authenticates successfully without issues. If you run into authentication errors, double-check the Infisical identity has the correct roles/permissions in Infisical.
</Step>
</Steps>

View File

@ -0,0 +1,142 @@
---
title: "KMIP Integration"
description: "Learn more about integrating with Infisical KMS using KMIP (Key Management Interoperability Protocol)."
---
<Note>
KMIP integration is an Enterprise-only feature. Please reach out to
sales@infisical.com if you have any questions.
</Note>
## Overview
Infisical KMS provides **Key Management Interoperability Protocol (KMIP)** support, enabling seamless integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the **KMIP 1.4 protocol**.
## Supported Operations
The Infisical KMIP server supports the following operations for **symmetric keys**:
- **Create** - Generate symmetric keys.
- **Register** - Register externally created keys.
- **Locate** - Find keys using attributes.
- **Get** - Retrieve keys securely.
- **Activate** - Enable keys for usage.
- **Revoke** - Revoke existing keys.
- **Destroy** - Permanently remove keys.
- **Get Attributes** - Retrieve metadata associated with keys.
- **Query** - Query server capabilities and supported operations.
## Benefits of KMIP Integration
Integrating Infisical KMS with KMIP-compatible clients provides the following benefits:
- **Standardized Key Management**: Allows interoperability with security and cryptographic applications that support KMIP.
- **Enterprise-Grade Security**: Utilizes Infisicals encryption mechanisms to securely store and manage keys.
- **Centralized Key Management**: Enables a unified approach for managing cryptographic keys across multiple environments.
## Compatibility
Infisical KMIP supports **KMIP versions 1.0 to 1.4**, ensuring compatibility with a wide range of clients and security tools.
## Secure Communication & Authorization
KMIP client-server communication is secured using **mutual TLS (mTLS)**, ensuring strong identity verification and encrypted data exchange via **PKI certificates**. Each KMIP entity must possess valid certificates signed by a trusted Root CA to establish trust.
For strong isolation, each Infisical organization has its own KMIP PKI (Public Key Infrastructure), ensuring that cryptographic operations and certificate authorities remain separate across organizations.
Infisical KMS enforces a **two-layer authorization model** for KMIP operations:
1. **KMIP Server Authorization** The KMIP server, acting as a proxy, must have the `proxy KMIP` permission to forward client requests to Infisical KMS. This is done using a **machine identity** attached to the KMIP server.
2. **KMIP Client Authorization** Clients must have the necessary KMIP-level permissions to perform specific key management operations.
By combining **mTLS for secure communication** and **machine identity-based proxying**, Infisical KMS ensures **strong authentication, controlled access, and centralized key management** for KMIP operations.
## Setup Instructions
### Setup KMIP for your organization
<Steps>
<Step title="Navigate to the organization settings > KMIP">
From there, press Setup KMIP.
![KMIP org navigate](/images/platform/kms/kmip/kmip-org-setup-navigation.png)
</Step>
<Step title="Configure KMIP PKI for the organization">
In the modal, select the desired key algorithm to use for the KMIP PKI of your organization. Press continue.
![KMIP org PKI setup](/images/platform/kms/kmip/kmip-org-setup-modal.png)
This generates the KMIP PKI for your organization. After this, you can proceed to setting up your KMIP server.
</Step>
</Steps>
### Deploying and Configuring the KMIP Server
Follow these steps to configure and deploy a KMIP server.
<Steps>
<Step title="Setup Machine Identity">
Configure a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) for the KMIP server to use.
![KMIP create machine identity](/images/platform/kms/kmip/kmip-create-mi.png)
Create a custom organization role and give it the **Proxy KMIP** permission.
![KMIP create custom role](/images/platform/kms/kmip/kmip-create-custom-role.png)
![KMIP assign proxy to role](/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png)
Assign the machine identity to the custom organization role. This allows the machine identity to serve KMIP client requests and forward them from your KMIP server to Infisical.
![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png)
</Step>
<Step title="Start up the KMIP server">
To deploy the KMIP server, use the Infisical CLIs `kmip start` command.
Before proceeding, make sure you have the [Infisical CLI installed](https://infisical.com/docs/cli/overview).
Once installed, launch the KMIP server with the following command:
```bash
infisical kmip start \
--identity-client-id=<machine-identity-client-id> \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_ID ENV variable
--identity-client-secret=<machine-identity-client-secret> \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET ENV variable
--domain=https://app.infisical.com \
--hostnames-or-ips="my-kmip-server.com"
```
The following flags are available for the `infisical kmip start` command::
- **listen-address** (default: localhost:5696): The address the KMIP server listens on.
- **identity-auth-method** (default: universal-auth): The authentication method for the machine identity.
- **identity-client-id**: The client ID of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` ENV variable.
- **identity-client-secret**: The client secret of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` ENV variable.
- **server-name** (default: "kmip-server"): The name of the KMIP server.
- **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid.
- **hostnames-or-ips:** A comma-separated list of hostnames or IPs the KMIP server will use (required).
</Step>
</Steps>
### Add and Configure KMIP Clients
<Steps>
<Step title="Navigate to the desired KMS project and select KMIP">
From there, press Add KMIP Client
![KMIP client overview](/images/platform/kms/kmip/kmip-client-overview.png)
</Step>
<Step title="Configure KMIP client">
In the modal, provide the details of your client. The selected permissions determine what KMIP operations can be performed in your KMS project.
![KMIP client modal](/images/platform/kms/kmip/kmip-client-modal.png)
</Step>
<Step title="Generate client certificate">
Once the KMIP client is created, you will have to generate a client certificate.
Press Generate Certificate.
![KMIP generate client cert](/images/platform/kms/kmip/kmip-client-generate-cert.png)
Provide the desired TTL and key algorithm to use and press Generate Client Certificate.
![KMIP client cert config](/images/platform/kms/kmip/kmip-client-cert-config-modal.png)
Configure your KMIP clients to use the generated client certificate, certificate chain and private key.
![KMIP client cert modal](/images/platform/kms/kmip/kmip-client-certificate-modal.png)
</Step>
</Steps>
## Additional Resources
- [KMIP 1.4 Specification](http://docs.oasis-open.org/kmip/spec/v1.4/os/kmip-spec-v1.4-os.html)

View File

@ -0,0 +1,36 @@
---
title: "Networking"
sidebarTitle: "Networking"
description: "Network configuration details for Infisical Cloud"
---
## Overview
When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services.
## Egress IP Addresses
Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services.
### US Region
To allow connections from Infisical US, add these IP addresses to your ingress rules:
- `3.213.63.16`
- `54.164.68.7`
### EU Region
To allow connections from Infisical EU, add these IP addresses to your ingress rules:
- `3.77.89.19`
- `3.125.209.189`
## Common Use Cases
You may need to allow Infisicals egress IPs if your services require inbound connections for:
- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials
- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services
- Secret integrations - When syncing secrets with third-party services like Azure Key Vault
- Native authentication with machine identities - When using methods like Kubernetes authentication

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

View File

@ -1,102 +1,237 @@
---
title: "Terraform Provider"
description: "Learn how to fetch Secrets From Infisical With Terraform."
url: "https://registry.terraform.io/providers/Infisical/infisical/latest/docs"
title: "Terraform"
description: "Learn how to fetch secrets from Infisical with Terraform using both traditional data sources and ephemeral resources"
---
{/*
This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform.
This guide demonstrates how to use Infisical to manage secrets in your Terraform infrastructure code, supporting both traditional data sources and ephemeral resources for enhanced security. It uses:
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets
- The [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest) to fetch secrets for your infrastructure
## Prerequisites
- Basic understanding of Terraform
- Install [Terraform](https://www.terraform.io/downloads.html)
Before you begin, make sure you have:
## Steps
- [Terraform](https://www.terraform.io/downloads.html) installed (v1.10.0+ for ephemeral resources)
- An Infisical account with access to a project
- Basic understanding of Terraform and infrastructure as code
### 1. Define Required Providers
## Project Setup
Specify `infisical` in the `required_providers` block within the `terraform` block of your configuration file. If you would like to use a specific version of the provider, uncomment and replace `<latest version>` with the version of the Infisical provider that you want to use.
### Configure Provider
```hcl main.tf
First, specify the Infisical provider in your Terraform configuration:
```hcl
terraform {
required_providers {
infisical = {
# version = <latest version>
source = "infisical/infisical"
}
}
}
```
### 2. Configure the Infisical Provider
### Authentication
Set up the Infisical provider by specifying the `host` and `service_token`. Replace `<>` in `service_token` with your actual token. The `host` is only required if you are using a self-hosted instance of Infisical.
Configure the provider using one of these authentication methods:
```hcl main.tf
#### Machine Identity (Recommended)
Using a Machine Identity, you can authenticate your Terraform provider using either [OIDC Auth](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) or [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) methods.
```hcl
provider "infisical" {
host = "https://app.infisical.com" # Only required if using a self-hosted instance of Infisical, default is https://app.infisical.com
client_id = "<>"
client_secret = "<>"
service_token = "<>" # DEPRECATED, USE MACHINE IDENTITY AUTH INSTEAD
host = "https://app.infisical.com" # Optional for cloud, required for self-hosted
auth {
universal { # or use oidc authentication method by providing an identity_id
client_id = var.infisical_client_id
client_secret = var.infisical_client_secret
}
}
}
```
Learn more about [machine identities](/documentation/platform/identities/machine-identities).
#### Service Token (Legacy)
<Warning>
Machine Identity authentication is strongly recommended as the secure and modern method. Service tokens are considered legacy and will be deprecated in a future release.
</Warning>
```hcl
provider "infisical" {
host = "https://app.infisical.com"
service_token = var.infisical_service_token
}
```
## Using Secrets in Terraform
Infisical provides two methods to fetch and use secrets in your Terraform configurations:
### Method 1: Ephemeral Resources (Recommended)
Ephemeral resources, introduced in Terraform v1.10, provide enhanced security by ensuring sensitive values are never persisted in state files. This is the recommended approach for handling secrets in your infrastructure code.
```hcl
# Fetch database credentials ephemerally
ephemeral "infisical_secret" "db_creds" {
name = "DB_CREDENTIALS"
env_slug = "prod"
workspace_id = var.infisical_workspace_id
folder_path = "/database"
}
# Use the credentials to configure a provider
provider "postgresql" {
host = data.aws_db_instance.example.address
port = data.aws_db_instance.example.port
username = jsondecode(ephemeral.infisical_secret.db_creds.value)["username"]
password = jsondecode(ephemeral.infisical_secret.db_creds.value)["password"]
}
```
Key benefits:
- Values are never stored in state files
- Secrets are fetched on-demand during each Terraform operation
- Perfect for GitOps workflows
- Improved security posture for your infrastructure as code
### Method 2: Data Sources
For backwards compatibility or when working with older Terraform versions, you can use the traditional data source approach:
```hcl
# Fetch all secrets in a folder
data "infisical_secrets" "my_secrets" {
env_slug = "dev"
workspace_id = var.infisical_workspace_id
folder_path = "/api"
}
# Use individual secrets
resource "aws_db_instance" "example" {
username = data.infisical_secrets.my_secrets.secrets["DB_USER"]
password = data.infisical_secrets.my_secrets.secrets["DB_PASS"]
}
```
<Warning>
It is recommended to use Terraform variables to pass your service token dynamically to avoid hard coding it
When using data sources, secret values are stored in Terraform's state file. Ensure your state file is properly secured.
</Warning>
### 3. Fetch Infisical Secrets
## Common Use Cases
Use the `infisical_secrets` data source to fetch your secrets. In this block, you must set the `env_slug` and `folder_path` to scope the secrets you want.
### Secure Database Credential Management
`env_slug` is the slug of the environment name. This slug name can be found under the project settings page on the Infisical dashboard.
`folder_path` is the path to the folder in a given environment. The path `/` for root of the environment where as `/folder1` is the folder at the root of the environment.
```hcl main.tf
data "infisical_secrets" "my-secrets" {
env_slug = "dev"
folder_path = "/some-folder/another-folder"
workspace_id = "your-project-id"
}
```
### 4. Define Outputs
As an example, we are going to output your fetched secrets. Replace `SECRET-NAME` with the actual name of your secret.
For a single secret:
```hcl main.tf
output "single-secret" {
value = data.infisical_secrets.my-secrets.secrets["SECRET-NAME"]
}
```
For all secrets:
Manage database credentials securely without exposing sensitive information in your state files:
```hcl
output "all-secrets" {
value = data.infisical_secrets.my-secrets.secrets
# Fetch database credentials securely
ephemeral "infisical_secret" "db_creds" {
name = "DB_CREDENTIALS"
env_slug = "prod"
workspace_id = var.infisical_workspace_id
folder_path = "/database"
}
# Use the credentials in your database instance
resource "aws_db_instance" "example" {
identifier = "my-database"
allocated_storage = 20
engine = "postgres"
engine_version = "14.0"
instance_class = "db.t3.micro"
# Securely inject credentials from Infisical
username = jsondecode(ephemeral.infisical_secret.db_creds.value)["username"]
password = jsondecode(ephemeral.infisical_secret.db_creds.value)["password"]
}
```
### 5. Run Terraform
### GitOps Workflow with OIDC
Once your configuration is complete, initialize your Terraform working directory:
To eliminate the need for static credentials, you can authenticate your workflow using [OpenID Connect (OIDC)](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) through providers like the [Infisical Secrets GitHub Action](https://github.com/Infisical/secrets-action).
Once authenticated, you can securely access secrets through the Infisical provider:
```bash
$ terraform init
```hcl
provider "infisical" {
# Auth credentials automatically injected from the environment
}
# Fetch deployment credentials
ephemeral "infisical_secret" "deploy_token" {
name = "DEPLOY_TOKEN"
env_slug = "prod"
workspace_id = var.infisical_workspace_id
folder_path = "/deployment"
}
```
For detailed instructions on setting up OIDC authentication with GitHub Actions, refer to our [GitHub Actions OIDC guide](https://infisical.com/docs/documentation/platform/identities/oidc-auth/github).
Then, run the plan command to view the fetched secrets:
## Best Practices
```bash
$ terraform plan
```
1. **Use Ephemeral Resources**: Whenever possible, use ephemeral resources instead of data sources for improved security.
Terraform will now fetch your secrets from Infisical and display them as output according to your configuration.
2. **Organize Secrets**: Structure your secrets in Infisical using folders to maintain clean separation:
```hcl
ephemeral "infisical_secret" "db_secret" {
folder_path = "/databases/postgresql" # Organized by service
# ...
}
```
## Conclusion
3. **Variable Usage**: Use Terraform variables for workspace IDs and environment slugs:
```hcl
variable "environment" {
description = "Environment (dev, staging, prod)"
type = string
}
You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). */}
ephemeral "infisical_secret" "secret" {
env_slug = var.environment
# ...
}
```
4. **Error Handling**: Add lifecycle blocks for critical secrets:
```hcl
ephemeral "infisical_secret" "critical_secret" {
# ...
lifecycle {
postcondition {
condition = length(self.value) > 0
error_message = "Critical secret must not be empty"
}
}
}
```
## FAQ
<AccordionGroup>
<Accordion title="What happens if I'm using an older version of Terraform?">
If you're using Terraform < v1.10.0, you'll need to use the data source approach.
Consider upgrading to take advantage of the enhanced security features provided
by ephemeral resources.
</Accordion>
<Accordion title="Can I mix ephemeral resources and data sources?">
Yes, you can use both in the same configuration. However, we recommend using
ephemeral resources for any sensitive values to ensure they're not stored in state.
</Accordion>
<Accordion title="How do I secure my state file when using data sources?">
When using data sources, follow Terraform's best practices for state management:
- Use remote state with encryption at rest
- Implement proper access controls
- Consider using state encryption
- Treat the state like a secret
Better yet, use ephemeral resources to avoid storing sensitive values in state entirely.
</Accordion>
</AccordionGroup>
See also:
- [Machine Identity setup guide](/documentation/platform/identities/machine-identities)
- [Terraform Provider Registry](https://registry.terraform.io/providers/Infisical/infisical/latest/docs)
- [GitOps Best Practices](https://www.infisical.com/blog/gitops-best-practices)

View File

@ -85,6 +85,10 @@
"documentation/guides/microsoft-power-apps",
"documentation/guides/organization-structure"
]
},
{
"group": "Setup",
"pages": ["documentation/setup/networking"]
}
]
},
@ -120,7 +124,8 @@
"pages": [
"documentation/platform/kms/overview",
"documentation/platform/kms/hsm-integration",
"documentation/platform/kms/kubernetes-encryption"
"documentation/platform/kms/kubernetes-encryption",
"documentation/platform/kms/kmip"
]
},
{
@ -232,7 +237,8 @@
"documentation/platform/identities/oidc-auth/general",
"documentation/platform/identities/oidc-auth/github",
"documentation/platform/identities/oidc-auth/circleci",
"documentation/platform/identities/oidc-auth/gitlab"
"documentation/platform/identities/oidc-auth/gitlab",
"documentation/platform/identities/oidc-auth/terraform-cloud"
]
},
"documentation/platform/mfa",
@ -285,7 +291,7 @@
"pages": [
"self-hosting/overview",
{
"group": "Containerized installation methods",
"group": "Installation methods",
"pages": [
"self-hosting/deployment-options/standalone-infisical",
"self-hosting/deployment-options/docker-swarm",
@ -293,12 +299,12 @@
"self-hosting/deployment-options/kubernetes-helm"
]
},
"self-hosting/guides/upgrading-infisical",
"self-hosting/configuration/envars",
"self-hosting/configuration/requirements",
{
"group": "Guides",
"pages": [
"self-hosting/configuration/schema-migrations",
"self-hosting/guides/mongo-to-postgres",
"self-hosting/guides/custom-certificates"
]
@ -633,7 +639,8 @@
"api-reference/endpoints/oidc-auth/attach",
"api-reference/endpoints/oidc-auth/retrieve",
"api-reference/endpoints/oidc-auth/update",
"api-reference/endpoints/oidc-auth/revoke"
"api-reference/endpoints/oidc-auth/revoke",
"integrations/frameworks/terraform-cloud"
]
},
{

View File

@ -4,8 +4,6 @@ sidebarTitle: "Go"
icon: "golang"
---
If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application.
- [Package](https://pkg.go.dev/github.com/infisical/go-sdk)
@ -57,7 +55,9 @@ func main() {
This example demonstrates how to use the Infisical Go SDK in a simple Go application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project.
<Warning>
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
We do not recommend hardcoding your [Machine Identity
Tokens](/platform/identities/overview). Setting it as an environment variable
would be best.
</Warning>
# Installation
@ -95,6 +95,10 @@ client := infisical.NewInfisicalClient(context.Background(), infisical.Config{
<ParamField query="SilentMode" type="boolean" default={false} optional>
Whether or not to suppress logs such as warnings from the token refreshing process. Defaults to false if not specified.
</ParamField>
<ParamField query="CacheExpiryInSeconds" type="number" default={0} optional>
Defines how long certain responses should be cached in memory, in seconds. When set to a positive value, responses from specific methods (like secret fetching) will be cached for this duration. Set to 0 to disable caching.
</ParamField>
</Expandable>
</ParamField>
@ -140,6 +144,7 @@ Call `.Auth().UniversalAuthLogin()` with empty arguments to use the following en
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
**Using the SDK directly**
```go
_, err := client.Auth().UniversalAuthLogin("CLIENT_ID", "CLIENT_SECRET")
@ -150,9 +155,12 @@ if err != nil {
```
#### GCP ID Token Auth
<Info>
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
Please note that this authentication method will only work if you're running
your application on Google Cloud Platform. Please [read
more](/documentation/platform/identities/gcp-auth) about this authentication
method.
</Info>
**Using environment variables**
@ -162,6 +170,7 @@ Call `.Auth().GcpIdTokenAuthLogin()` with empty arguments to use the following e
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```go
_, err := client.Auth().GcpIdTokenAuthLogin("YOUR_MACHINE_IDENTITY_ID")
@ -181,6 +190,7 @@ Call `.Auth().GcpIamAuthLogin()` with empty arguments to use the following envir
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
**Using the SDK directly**
```go
_, err = client.Auth().GcpIamAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_KEY_FILE_PATH")
@ -191,9 +201,12 @@ if err != nil {
```
#### AWS IAM Auth
<Info>
Please note that this authentication method will only work if you're running your application on AWS.
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
Please note that this authentication method will only work if you're running
your application on AWS. Please [read
more](/documentation/platform/identities/aws-auth) about this authentication
method.
</Info>
**Using environment variables**
@ -203,6 +216,7 @@ Call `.Auth().AwsIamAuthLogin()` with empty arguments to use the following envir
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```go
_, err = client.Auth().AwsIamAuthLogin("MACHINE_IDENTITY_ID")
@ -212,11 +226,13 @@ if err != nil {
}
```
#### Azure Auth
<Info>
Please note that this authentication method will only work if you're running your application on Azure.
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
Please note that this authentication method will only work if you're running
your application on Azure. Please [read
more](/documentation/platform/identities/azure-auth) about this authentication
method.
</Info>
**Using environment variables**
@ -226,6 +242,7 @@ Call `.Auth().AzureAuthLogin()` with empty arguments to use the following enviro
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
**Using the SDK directly**
```go
_, err = client.Auth().AzureAuthLogin("MACHINE_IDENTITY_ID")
@ -236,9 +253,12 @@ if err != nil {
```
#### Kubernetes Auth
<Info>
Please note that this authentication method will only work if you're running your application on Kubernetes.
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
Please note that this authentication method will only work if you're running
your application on Kubernetes. Please [read
more](/documentation/platform/identities/kubernetes-auth) about this
authentication method.
</Info>
**Using environment variables**
@ -249,6 +269,7 @@ Call `.Auth().KubernetesAuthLogin()` with empty arguments to use the following e
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
**Using the SDK directly**
```go
// Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed
_, err = client.Auth().KubernetesAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_TOKEN_PATH")
@ -262,6 +283,7 @@ if err != nil {
## Working With Secrets
### List Secrets
`client.Secrets().List(options)`
Retrieve all secrets within the Infisical project and environment that client is connected to.
@ -275,7 +297,7 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object">
<Expandable title="properties">
@ -311,7 +333,9 @@ secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
</ParamField>
###
### Retrieve Secret
`client.Secrets().Retrieve(options)`
Retrieve a secret from Infisical. By default `Secrets().Retrieve()` fetches and returns a shared secret.
@ -324,30 +348,34 @@ secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to retrieve.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be fetched from.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to retrieve.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets
should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be fetched from.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not
specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
###
### Create Secret
`client.Secrets().Create(options)`
Create a new secret in Infisical.
@ -363,36 +391,38 @@ secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to create.
</ParamField>
<ParamField query="SecretValue" type="string" required>
The value of the secret.
</ParamField>
<ParamField query="SecretComment" type="string" optional>
A comment for the secret.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be created.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to create.
</ParamField>
<ParamField query="SecretValue" type="string" required>
The value of the secret.
</ParamField>
<ParamField query="SecretComment" type="string" optional>
A comment for the secret.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets
should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be created.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not
specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
###
### Update Secret
`client.Secrets().Update(options)`
@ -409,36 +439,45 @@ secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to update.
</ParamField>
<ParamField query="NewSecretValue" type="string" required>
The new value of the secret.
</ParamField>
<ParamField query="NewSkipMultilineEncoding" type="boolean" default="false" optional>
Whether or not to skip multiline encoding for the new secret value.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be updated.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
<Expandable title="properties">
<ParamField query="SecretKey" type="string" required>
The key of the secret to update.
</ParamField>
<ParamField query="NewSecretValue" type="string" required>
The new value of the secret.
</ParamField>
<ParamField
query="NewSkipMultilineEncoding"
type="boolean"
default="false"
optional
>
Whether or not to skip multiline encoding for the new secret value.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets
should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be updated.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not
specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
###
### Delete Secret
`client.Secrets().Delete(options)`
Delete a secret in Infisical.
@ -451,33 +490,106 @@ secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="SecretKey" type="string">
The key of the secret to update.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be deleted.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField>
</Expandable>
<Expandable title="properties">
<ParamField query="SecretKey" type="string">
The key of the secret to update.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets
should be fetched from.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be deleted.
</ParamField>
<ParamField query="Type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not
specified, the default value is "shared".
</ParamField>
</Expandable>
</ParamField>
## Working With folders
### Batch Create Secrets
`client.Secrets().Batch().Create(options)`
Create multiple secrets in Infisical.
```go
createdSecrets, err := client.Secrets().Batch().Create(infisical.BatchCreateSecretsOptions{
Environment: "<environment-slug>",
SecretPath: "<secret-path>",
ProjectID: "<project-id>",
Secrets: []infisical.BatchCreateSecret{
{
SecretKey: "SECRET-1",
SecretValue: "test-value-1",
},
{
SecretKey: "SECRET-2",
SecretValue: "test-value-2",
},
},
})
```
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField>
<ParamField query="ProjectID" type="string" required>
The project ID where the secret lives in.
</ParamField>
<ParamField query="SecretPath" type="string" optional>
The path from where secret should be created.
</ParamField>
<ParamField query="Secrets" type="array" required>
<Expandable>
<ParamField query="SecretKey" type="string" required>
The key of the secret to create.
</ParamField>
<ParamField query="SecretValue" type="string" required>
The value of the secret.
</ParamField>
<ParamField query="SecretComment" type="string" optional>
The comment to add to the secret.
</ParamField>
<ParamField query="SkipMultiLineEncoding" type="boolean" optional>
Whether or not to skip multiline encoding for the secret value.
</ParamField>
<ParamField query="TagIDs" type="string[]" optional>
The tag IDs to associate with the secret.
</ParamField>
<ParamField query="SecretMetadata" type="object" optional>
<Expandable>
<ParamField query="Key" type="string" required>
The key of the metadata.
</ParamField>
<ParamField query="Value" type="string" required>
The value of the metadata.
</ParamField>
</Expandable>
</ParamField>
</Expandable>
</ParamField>
</Expandable>
</ParamField>
## Working With Folders
###
### List Folders
`client.Folders().List(options)`
Retrieve all within the Infisical project and environment that client is connected to.
@ -490,7 +602,7 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object">
<Expandable title="properties">
@ -510,7 +622,9 @@ folders, err := client.Folders().List(infisical.ListFoldersOptions{
</ParamField>
###
### Create Folder
`client.Folders().Create(options)`
Create a new folder in Infisical.
@ -524,28 +638,30 @@ folder, err := client.Folders().Create(infisical.CreateFolderOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="ProjectID" type="string" required>
The ID of the project where the folder will be created.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment where the folder will be created.
</ParamField>
<ParamField query="Path" type="string" optional>
The path to create the folder in. The root path is `/`.
</ParamField>
<ParamField query="Name" type="string" optional>
The name of the folder to create.
</ParamField>
</Expandable>
<Expandable title="properties">
<ParamField query="ProjectID" type="string" required>
The ID of the project where the folder will be created.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment where the folder will be
created.
</ParamField>
<ParamField query="Path" type="string" optional>
The path to create the folder in. The root path is `/`.
</ParamField>
<ParamField query="Name" type="string" optional>
The name of the folder to create.
</ParamField>
</Expandable>
</ParamField>
###
### Update Folder
`client.Folders().Update(options)`
Update an existing folder in Infisical.
@ -560,30 +676,33 @@ folder, err := client.Folders().Update(infisical.UpdateFolderOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
<ParamField query="ProjectID" type="string" required>
The ID of the project where the folder will be updated.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where the folder lives in.
</ParamField>
<ParamField query="Path" type="string" optional>
The path from where the folder should be updated.
</ParamField>
<ParamField query="FolderID" type="string" required>
The ID of the folder to update.
</ParamField>
<ParamField query="NewName" type="string" required>
The new name of the folder.
</ParamField>
</Expandable>
<Expandable title="properties">
<ParamField query="ProjectID" type="string" required>
The ID of the project where the folder will be updated.
</ParamField>
<ParamField query="Environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where the folder
lives in.
</ParamField>
<ParamField query="Path" type="string" optional>
The path from where the folder should be updated.
</ParamField>
<ParamField query="FolderID" type="string" required>
The ID of the folder to update.
</ParamField>
<ParamField query="NewName" type="string" required>
The new name of the folder.
</ParamField>
</Expandable>
</ParamField>
###
### Delete Folder
`client.Folders().Delete(options)`
Delete a folder in Infisical.
@ -599,7 +718,7 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
})
```
### Parameters
#### Parameters
<ParamField query="Parameters" type="object" optional>
<Expandable title="properties">
@ -620,6 +739,5 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
The path from where the folder should be deleted.
</ParamField>
</Expandable>
</ParamField>

View File

@ -22,33 +22,34 @@ The actual resource requirements will vary in direct proportion to the operation
Infisical doesnt require file storage as all persisted data is saved in the database.
However, its logs and metrics are saved to disk for later viewing. As a result, we recommend provisioning 1-2 GB of storage.
### CPU
### CPU and Memory (Per Container/Instance)
CPU requirements vary heavily on the volume of secret operations (reads and writes) you anticipate.
Processing large volumes of secrets frequently and consistently will require higher CPU.
Infisical is stateless and scales horizontally by running across multiple containers/instances. Each instance typically does **not** need more than **24 CPU cores** and **48 GB** of memory.
If you need additional capacity, simply increase the **number** of containers/instances running in parallel.
Recommended minimum CPU hardware for different sizes of deployments:
| **Deployment Size** | **CPU (Cores, per container)** | **Memory (GB, per container)** | **Recommended Number of Containers** |
|---------------------|--------------------------------|--------------------------------|--------------------------------------|
| **Small** | 2 | 4 | 2+ |
| **Medium** | 24 | 48 | 5+ |
| **Large** | 24 | 48 | 10+ |
- **small:** 2-4 core is the **recommended** minimum
- **large:** 4-8 cores are suitable for larger deployments
### Memory Allocation
Memory needs depend on expected workload, including factors like user activity, automation level, and the frequency of secret operations.
Recommended minimum memory hardware for different sizes of deployments:
- **small:** 4-8 GB is the **recommended** minimum
- **large:** 16-32 GB are suitable for larger deployments
> **Note:**
> - Adding more containers (horizontal scaling) is generally the best way to handle spikes in secret operations.
> - If you prefer, you can increase CPU/memory on a single container (vertical scaling), but horizontal scaling is more flexible and resilient.
## Database & caching layer
### Postgres
PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility.
The compute required for Postgres is largely dependent on the number of secret operations (reads and writes) you expect. The more frequently you read and write secrets, the more compute you will need.
You'll notice that storage requirements are high and this is because audit logs are by default stored in the database.
Recommended resource allocation based on deployment size:
- **small:** 2 vCPU / 8 GB RAM / 20 GB Disk
- **large:** 4vCPU / 16 GB RAM / 100 GB Disk
Recommended resource allocation based on deployment size. You may require more resources if you have a large number of secrets or high transaction volume:
- **small:** 2 vCPU / 8 GB RAM / 100 GB Disk
- **medium:** 4vCPU / 16 GB RAM / 200 GB Disk
- **large:** 8vCPU / 32 GB RAM / 500 GB Disk
### Redis

View File

@ -1,60 +0,0 @@
---
title: "Schema migration"
description: "Learn how to run Postgres schema migrations."
---
Running schema migrations is a requirement before deploying Infisical.
Each time you decide to upgrade your version of Infisical, it's necessary to run schema migrations for that specific version.
The guide below outlines a step-by-step guide to help you manually run schema migrations for Infisical.
### Prerequisites
- Docker installed on your machine
- An active PostgreSQL database
- Postgres database connection string
<Steps>
<Step title="Pull the Infisical Docker Image">
First, ensure you have the correct version of the Infisical Docker image. You can pull it from Docker Hub using the following command:
```bash
docker pull infisical/infisical:<version>
```
Replace `<version>` with the specific version number you intend to deploy. View available versions [here](https://hub.docker.com/r/infisical/infisical/tags)
</Step>
<Step title="Set Up the Environment Variable">
The Docker image requires a `DB_CONNECTION_URI` environment variable. This connection string should point to your PostgreSQL database. The format generally looks like this: `postgresql://username:password@host:port/database`.
</Step>
<Step title="Run the Migration ">
To run the schema migration for the version of Infisical you want to deploy, use the following Docker command:
```bash
docker run --env DB_CONNECTION_URI=<your_connection_string> infisical/infisical:<version> npm run migration:latest
```
Replace `<your_connection_string>` with your actual PostgreSQL connection string, and `<version>` with the desired version number.
</Step>
<Step title="Verify the Migration">
After running the migration, it's good practice to check if the migration was successful. You can do this by checking the logs or accessing your database to ensure the schema has been updated accordingly.
</Step>
<Step title="Rollback If Needed">
If you need to rollback a migration by one step, use the following command:
```bash
docker run --env DB_CONNECTION_URI=<your_connection_string> infisical/infisical:<version> npm run migration:rollback
```
</Step>
<Step title="Repeat for Each Version">
It's important to run schema migrations for each version of the Infisical you deploy. For instance, if you're updating from `infisical/infisical:1` to `infisical/infisical:2`, ensure you run the schema migrations for `infisical/infisical:2` before deploying it.
</Step>
</Steps>
<Tip>
In a production setting, we recommend a more structured approach to deploying migrations prior to upgrading Infisical. This can be accomplished via CI automation.
</Tip>
### Additional discussion
- Always back up your database before running migrations, especially in a production environment.
- Test the migration process in a staging environment before applying it to production.
- Keep track of the versions and their corresponding migrations to avoid any inconsistencies.

View File

@ -157,23 +157,6 @@ The [Docker stack file](https://github.com/Infisical/infisical/tree/main/docker-
3lznscvk7k5t infisical_spolo2 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
v04ml7rz2j5q infisical_spolo3 replicated 1/1 ghcr.io/zalando/spilo-16:3.2-p2
```
<Note>
You'll notice that service `infisical_infisical` will not be in running state.
This is expected as the database does not yet have the desired schemas.
Once the database schema migrations have been successfully applied, this issue should be resolved.
</Note>
</Step>
<Step title="Run schema migrations">
Run the schema migration to initialize the database. Follow the [guide here](/self-hosting/configuration/schema-migrations) to learn how.
To run the migrations, you'll need to connect to the Postgres instance deployed on your Docker swarm. The default Postgres user credentials are defined in the Docker swarm: username: `postgres`, password: `postgres` and database: `postgres`.
We recommend you change these credentials when deploying to production and creating a separate DB for Infisical.
<Info>
After running the schema migrations, be sure to update the `.env` file to have the correct `DB_CONNECTION_URI`.
</Info>
</Step>
<Step title="View service status">

View File

@ -78,18 +78,6 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
</Tabs>
</Step>
<Step title="Database schema migration ">
Infisical relies on a relational database, which means that database schemas need to be migrated before the instance can become operational.
To automate this process, the chart includes a option named `infisical.autoDatabaseSchemaMigration`.
When this option is enabled, a deployment/upgrade will only occur _after_ a successful schema migration.
<Info>
If you are using in-cluster Postgres, you may notice the migration job failing initially.
This is expected as it is waiting for the database to be in ready state.
</Info>
</Step>
<Step title="Routing traffic to Infisical">
By default, this chart uses Nginx as its Ingress controller to direct traffic to Infisical services.

View File

@ -22,11 +22,6 @@ The following guide provides a detailed step-by-step walkthrough on how you can
Remember to replace `<version>` with the docker image tag of your choice.
</Step>
<Step title="Run Postgres schema migration ">
Before you can start the instance of Infisical, you need to run the database schema migrations.
Follow the step by [step guide here](/self-hosting/configuration/schema-migrations) on running schema migrations for Infisical.
</Step>
<Step title="Start Infisical">
For a minimal installation of Infisical, you must configure `ENCRYPTION_KEY`, `AUTH_SECRET`, `DB_CONNECTION_URI`, `SITE_URL`, and `REDIS_URL`. [View all available configurations](/self-hosting/configuration/envars).

View File

@ -0,0 +1,57 @@
---
title: "Upgrade Infisical Instance"
description: "How to upgrade Infisical self-hosted instance"
---
Keeping your Infisical instance up to date is key to making sure you receive the latest performance improvements, security patches, and feature updates.
We release updates approximately once a week, which may include new features, bug fixes, performance enhancements, and critical security patches.
Since secrets management is a critical component of your infrastructure, we aim to avoid disruptive changes that will impact fetching secrets in downstream clients.
If a release requires specific attention, a note will be attached to the corresponding [release](https://github.com/Infisical/infisical/releases) version.
During an upgrade, two key components are updated:
- **Infisical Application:** The core application code is updated.
- **PostgreSQL Database Schema:** Schema migrations run automatically to ensure your database remains in sync with the updated application.
> **Before You Upgrade:**
> **Always back up your database.** While our automated migration system is robust, having a backup ensures you can recover quickly in the event of an issue.
## Automated Schema Migrations
In previous versions (prior to `v0.111.0-postgres`), schema migrations had to be executed manually before starting the application.
Now, migrations run automatically during boot-up. This improvement streamlines the upgrade process, reduces manual steps, and minimizes the risk of inconsistencies between your database schema and application code.
### Benefits of Automated Migrations
- **Seamless Integration:**
Migrations are now part of the boot-up process, removing the need for manual intervention.
- **Synchronous Upgrades:**
In multi-instance deployments, one instance acquires a lock and performs the migration while the others wait. This ensures that if a migration fails, the rollout is halted to prevent inconsistencies.
- **Reduced Room for Error:**
Automatic migrations help ensure that your database schema always remains in sync with your application code.
## Upgrade Steps
1. **Back Up Your Data:**
- Ensure you have a complete backup of your Postgres database.
- Verify that your backup is current and accessible.
2. **Select the Upgrade Version:**
- Visit the [Infisical releases page](https://github.com/Infisical/infisical/releases) for a list of available versions.
- Look for releases with the prefix `infisical/` as there are other releases that are not related to the Infisical instance.
3. **Start the Upgrade Process:**
- Launch the new version of Infisical. During startup, the application will automatically compare the current database schema with the updated schema in the code.
- If any differences are detected, Infisical will apply the necessary migrations automatically.
4. **Multi-Instance Coordination:**
- In environments with multiple instances, one instance will acquire a lock and perform the migration while the other instances wait.
- Once the migration is complete, all instances will operate with the updated schema.
5. **Verify the Upgrade:**
- Review the logs for any migration errors or warnings.

View File

@ -48,9 +48,3 @@ This ensures that if there is a failure in one availability zone, the working re
<Accordion title="Can Infisical run in an air-gapped environment without any internet access?" defaultOpen >
Yes, Infisical can function in an air-gapped environment. To do so, update your ECS task to use the publicly available AWS Elastic Container Registry (ECR) image instead of the default Docker Hub image. Additionally, it's necessary to configure VPC endpoints, which allows your system to access AWS ECR via a private network route instead of the internet, ensuring all connectivity remains within the secure, private network.
</Accordion>
<Accordion title="Since RDS is in a private subnet, how do run the Postgres schema migrations?">
Since the Amazon RDS instance is housed within a private network to enhance security, it is not directly accessible from the internet. This means that in order to run the required [Postgres schema migrations](/self-hosting/configuration/schema-migrations), you need to connect to this instance of RDS. There are many approaches you can take:
- To automate schema migrations, you may setup CI/CD pipeline with access to the same RDS network to run the schema migrations before making deployment to ECS. This ensures that if migrations fail, your Infisical instances continues to run.
- If you would like to run the migrations manually, consider using AWS Systems Manager Session Manager to access the RDS within the VPC on your local machine.
- If your organization already has mechanisms in place for secure access to the VPC, such as VPNs or Direct Connect, these can also be utilized for performing database migrations manually.
</Accordion>

File diff suppressed because one or more lines are too long

View File

@ -10,9 +10,9 @@ type Props = {
};
export const SecretSyncSelect = ({ onSelect }: Props) => {
const { isLoading, data: secretSyncOptions } = useSecretSyncOptions();
const { isPending, data: secretSyncOptions } = useSecretSyncOptions();
if (isLoading) {
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />

View File

@ -23,7 +23,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[destination];
const { data: availableConnections, isLoading } = useListAvailableAppConnections(app);
const { data: availableConnections, isPending } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
@ -54,7 +54,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
onChange(newValue);
if (callback) callback();
}}
isLoading={isLoading}
isLoading={isPending}
options={availableConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}

View File

@ -1,5 +1,5 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { DotLottie, DotLottieReact } from "@lottiefiles/dotlottie-react";
import { DotLottie, DotLottieReact, Mode } from "@lottiefiles/dotlottie-react";
import { twMerge } from "tailwind-merge";
export type MenuProps = {
@ -16,6 +16,7 @@ export type MenuItemProps<T extends ElementType> = {
as?: T;
children: ReactNode;
icon?: string;
iconMode?: Mode;
description?: ReactNode;
isDisabled?: boolean;
isSelected?: boolean;
@ -26,6 +27,7 @@ export type MenuItemProps<T extends ElementType> = {
export const MenuItem = <T extends ElementType = "button">({
children,
icon,
iconMode,
className,
isDisabled,
isSelected,
@ -62,6 +64,7 @@ export const MenuItem = <T extends ElementType = "button">({
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
mode={iconMode}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"

View File

@ -24,7 +24,8 @@ export enum OrgPermissionSubjects {
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections"
AppConnections = "app-connections",
Kmip = "kmip"
}
export enum OrgPermissionAdminConsoleAction {
@ -39,6 +40,11 @@ export enum OrgPermissionAppConnectionActions {
Connect = "connect"
}
export enum OrgPermissionKmipActions {
Proxy = "proxy",
Setup = "setup"
}
export type AppConnectionSubjectFields = {
connectionId: string;
};
@ -61,7 +67,8 @@ export type OrgPermissionSet =
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections];
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
// TODO(scott): add back once org UI refactored
// | [
// OrgPermissionAppConnectionActions,

View File

@ -4,5 +4,6 @@ export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSub
} from "./types";

View File

@ -24,6 +24,14 @@ export enum ProjectPermissionCmekActions {
Decrypt = "decrypt"
}
export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
DeleteClients = "delete-clients",
ReadClients = "read-clients",
GenerateClientCertificates = "generate-client-certificates"
}
export enum ProjectPermissionSecretSyncActions {
Read = "read",
Create = "create",
@ -102,7 +110,8 @@ export enum ProjectPermissionSub {
PkiCollections = "pki-collections",
Kms = "kms",
Cmek = "cmek",
SecretSyncs = "secret-syncs"
SecretSyncs = "secret-syncs",
Kmip = "kmip"
}
export type SecretSubjectFields = {
@ -190,5 +199,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@ -10,6 +10,7 @@ export {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";

View File

@ -0,0 +1,6 @@
import FileSaver from "file-saver";
export const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};

View File

@ -121,7 +121,24 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER]:
"OIDC group membership mapping assigned user to groups",
[EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER]:
"OIDC group membership mapping removed user from groups"
"OIDC group membership mapping removed user from groups",
[EventType.CREATE_KMIP_CLIENT]: "Create KMIP client",
[EventType.UPDATE_KMIP_CLIENT]: "Update KMIP client",
[EventType.DELETE_KMIP_CLIENT]: "Delete KMIP client",
[EventType.GET_KMIP_CLIENT]: "Get KMIP client",
[EventType.GET_KMIP_CLIENTS]: "Get KMIP clients",
[EventType.CREATE_KMIP_CLIENT_CERTIFICATE]: "Create KMIP client certificate",
[EventType.SETUP_KMIP]: "Setup KMIP configuration",
[EventType.GET_KMIP]: "Get KMIP configuration",
[EventType.REGISTER_KMIP_SERVER]: "Register KMIP server",
[EventType.KMIP_OPERATION_CREATE]: "KMIP operation create",
[EventType.KMIP_OPERATION_GET]: "KMIP operation get",
[EventType.KMIP_OPERATION_DESTROY]: "KMIP operation destroy",
[EventType.KMIP_OPERATION_GET_ATTRIBUTES]: "KMIP operation get attributes",
[EventType.KMIP_OPERATION_ACTIVATE]: "KMIP operation activate",
[EventType.KMIP_OPERATION_REVOKE]: "KMIP operation revoke",
[EventType.KMIP_OPERATION_LOCATE]: "KMIP operation locate",
[EventType.KMIP_OPERATION_REGISTER]: "KMIP operation register"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

View File

@ -1,5 +1,6 @@
export enum ActorType {
PLATFORM = "platform",
KMIP_CLIENT = "kmipClient",
USER = "user",
SERVICE = "service",
IDENTITY = "identity",
@ -132,5 +133,22 @@ export enum EventType {
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client",
UPDATE_KMIP_CLIENT = "update-kmip-client",
DELETE_KMIP_CLIENT = "delete-kmip-client",
GET_KMIP_CLIENT = "get-kmip-client",
GET_KMIP_CLIENTS = "get-kmip-clients",
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate",
SETUP_KMIP = "setup-kmip",
GET_KMIP = "get-kmip",
REGISTER_KMIP_SERVER = "register-kmip-server",
KMIP_OPERATION_CREATE = "kmip-operation-create",
KMIP_OPERATION_GET = "kmip-operation-get",
KMIP_OPERATION_DESTROY = "kmip-operation-destroy",
KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes",
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register"
}

View File

@ -30,6 +30,10 @@ interface IdentityActorMetadata {
identityId: string;
name: string;
}
interface KmipClientActorMetadata {
clientId: string;
name: string;
}
interface UserActor {
type: ActorType.USER;
@ -51,11 +55,22 @@ export interface PlatformActor {
metadata: object;
}
export interface KmipClientActor {
type: ActorType.KMIP_CLIENT;
metadata: KmipClientActorMetadata;
}
export interface UnknownUserActor {
type: ActorType.UNKNOWN_USER;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor | UnknownUserActor;
export type Actor =
| UserActor
| ServiceActor
| IdentityActor
| PlatformActor
| UnknownUserActor
| KmipClientActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;

View File

@ -0,0 +1,2 @@
export * from "./mutation";
export * from "./queries";

View File

@ -0,0 +1,90 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { kmipKeys } from "./queries";
import {
KmipClientCertificate,
TCreateKmipClient,
TDeleteKmipClient,
TGenerateKmipClientCertificate,
TSetupOrgKmipDTO,
TUpdateKmipClient
} from "./types";
export const useCreateKmipClient = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TCreateKmipClient) => {
const { data } = await apiRequest.post("/api/v1/kmip/clients", payload);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
});
}
});
};
export const useUpdateKmipClient = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, name, description, permissions }: TUpdateKmipClient) => {
const { data } = await apiRequest.patch(`/api/v1/kmip/clients/${id}`, {
name,
description,
permissions
});
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
});
}
});
};
export const useDeleteKmipClients = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id }: TDeleteKmipClient) => {
const { data } = await apiRequest.delete(`/api/v1/kmip/clients/${id}`);
return data;
},
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
});
}
});
};
export const useGenerateKmipClientCertificate = () => {
return useMutation({
mutationFn: async (payload: TGenerateKmipClientCertificate) => {
const { data } = await apiRequest.post<KmipClientCertificate>(
`/api/v1/kmip/clients/${payload.clientId}/certificates`,
payload
);
return data;
}
});
};
export const useSetupOrgKmip = (orgId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TSetupOrgKmipDTO) => {
await apiRequest.post("/api/v1/kmip", payload);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: kmipKeys.getOrgKmip(orgId) });
}
});
};

View File

@ -0,0 +1,69 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "../generic/types";
import {
KmipClientOrderBy,
OrgKmipConfig,
TListProjectKmipClientsDTO,
TProjectKmipClientList
} from "./types";
export const kmipKeys = {
getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) =>
[projectId, filters] as const,
getOrgKmip: (orgId: string) => [{ orgId }, "org-kmip-config"] as const
};
export const useGetKmipClientsByProjectId = (
{
projectId,
offset = 0,
limit = 100,
orderBy = KmipClientOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListProjectKmipClientsDTO,
options?: Omit<
UseQueryOptions<
TProjectKmipClientList,
unknown,
TProjectKmipClientList,
ReturnType<typeof kmipKeys.getKmipClientsByProjectId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: kmipKeys.getKmipClientsByProjectId({
projectId,
offset,
limit,
orderBy,
orderDirection,
search
}),
queryFn: async () => {
const { data } = await apiRequest.get<TProjectKmipClientList>("/api/v1/kmip/clients", {
params: { projectId, offset, limit, search, orderBy, orderDirection }
});
return data;
},
enabled: Boolean(projectId) && (options?.enabled ?? true),
placeholderData: (previousData) => previousData,
...options
});
};
export const useGetOrgKmipConfig = (orgId: string) => {
return useQuery({
queryKey: kmipKeys.getOrgKmip(orgId),
queryFn: async () => {
const { data } = await apiRequest.get<OrgKmipConfig>("/api/v1/kmip");
return data;
}
});
};

View File

@ -0,0 +1,81 @@
import { CertKeyAlgorithm } from "../certificates/enums";
import { OrderByDirection } from "../generic/types";
export enum KmipPermission {
Create = "create",
Locate = "locate",
Check = "check",
Get = "get",
GetAttributes = "get-attributes",
Activate = "activate",
Revoke = "revoke",
Destroy = "destroy",
Register = "register"
}
export type TKmipClient = {
id: string;
name: string;
description?: string;
permissions: KmipPermission[];
projectId: string;
};
type ProjectRef = { projectId: string };
type KeyRef = { id: string };
export type TCreateKmipClient = Pick<TKmipClient, "name" | "description" | "permissions"> &
ProjectRef;
export type TUpdateKmipClient = KeyRef &
Partial<Pick<TKmipClient, "name" | "description" | "permissions">> &
ProjectRef;
export type TProjectKmipClientList = {
kmipClients: TKmipClient[];
totalCount: number;
};
export type TGenerateKmipClientCertificate = {
keyAlgorithm: CertKeyAlgorithm;
ttl: string;
clientId: string;
};
export type KmipClientCertificate = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};
export type TDeleteKmipClient = KeyRef & ProjectRef;
export type TListProjectKmipClientsDTO = {
projectId: string;
offset?: number;
limit?: number;
orderBy?: KmipClientOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export enum KmipClientOrderBy {
Name = "name"
}
export type OrgKmipConfig = {
serverCertificateChain: string;
clientCertificateChain: string;
};
export type TSetupOrgKmipDTO = {
caKeyAlgorithm: CertKeyAlgorithm;
};
export type OrgKmipServerCert = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
};

View File

@ -46,4 +46,5 @@ export type SubscriptionPlan = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: boolean;
kmip: boolean;
};

View File

@ -3,6 +3,7 @@ import { AxiosError } from "axios";
import { apiRequest } from "@app/config/request";
import { SessionStorageKeys } from "@app/const";
import { queryClient as qc } from "@app/hooks/api/reactQuery";
import { APIKeyDataV2 } from "../apiKeys/types";
import { MfaMethod } from "../auth/types";
@ -27,7 +28,6 @@ import {
export const fetchUserDetails = async () => {
const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user");
return data.user;
};
@ -278,30 +278,33 @@ export const useRegisterUserAction = () => {
});
};
export const useLogoutUser = (keepQueryClient?: boolean) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
await apiRequest.post("/api/v1/auth/logout");
},
onSuccess: () => {
setAuthToken("");
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
export const logoutUser = async () => {
await apiRequest.post("/api/v1/auth/logout");
};
if (!keepQueryClient) {
queryClient.clear();
}
}
// Utility function to clear session storage and query cache
export const clearSession = (keepQueryClient?: boolean) => {
setAuthToken(""); // Clear authentication token
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN);
if (!keepQueryClient) {
qc.clear(); // Clear React Query cache
}
};
export const useLogoutUser = (keepQueryClient?: boolean) => {
return useMutation({
mutationFn: logoutUser,
onSuccess: () => clearSession(keepQueryClient)
});
};

View File

@ -105,6 +105,20 @@ export const ProjectLayout = () => {
)}
</Link>
)}
{isCmek && (
<Link
to={`/${ProjectType.KMS}/$projectId/kmip` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="key-user" iconMode="reverse">
KMIP
</MenuItem>
)}
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}

View File

@ -40,7 +40,8 @@ enum TabSections {
Auth = "auth",
RateLimit = "rate-limit",
Integrations = "integrations",
Users = "users"
Users = "users",
Kmip = "kmip"
}
enum SignUpModes {

View File

@ -33,6 +33,7 @@ export const LoginPage = () => {
console.log("Error - Not logged in yet");
}
};
if (isLoggedIn()) {
handleRedirects();
}

View File

@ -250,20 +250,24 @@ export const SignupInvitePage = () => {
setStep(2);
} else {
const redirectExistingUser = async () => {
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
organizationId
});
try {
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
organizationId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
toggleShowMfa.on();
setMfaSuccessCallback(() => redirectExistingUser);
return;
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
toggleShowMfa.on();
setMfaSuccessCallback(() => redirectExistingUser);
return;
}
// user will be redirected to dashboard
// if not logged in gets kicked out to login
await navigateUserToOrg(navigate, organizationId);
} catch (err) {
navigate({ to: "/login" });
}
// user will be redirected to dashboard
// if not logged in gets kicked out to login
await navigateUserToOrg(navigate, organizationId);
};
await redirectExistingUser();

View File

@ -0,0 +1,36 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context";
import { KmipClientTable } from "./components/KmipClientTable";
export const KmipPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="KMIP"
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
/>
<ProjectPermissionCan
passThrough={false}
renderGuardBanner
I={ProjectPermissionKmipActions.ReadClients}
a={ProjectPermissionSub.Kmip}
>
<KmipClientTable />
</ProjectPermissionCan>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,147 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { useGenerateKmipClientCertificate } from "@app/hooks/api/kmip";
import { KmipClientCertificate, TKmipClient } from "@app/hooks/api/kmip/types";
const formSchema = z.object({
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
kmipClient?: TKmipClient | null;
displayNewClientCertificate: (certificate: KmipClientCertificate) => void;
};
type FormProps = Pick<Props, "kmipClient" | "displayNewClientCertificate"> & {
onComplete: () => void;
};
const KmipClientCertificateForm = ({
displayNewClientCertificate,
kmipClient,
onComplete
}: FormProps) => {
const { mutateAsync: createKmipClientCertificate } = useGenerateKmipClientCertificate();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const handleKmipClientSubmit = async (payload: FormData) => {
if (!kmipClient) {
return;
}
const certificate = await createKmipClientCertificate({
...payload,
clientId: kmipClient?.id
});
createNotification({
text: "Successfully created KMIP client certificate",
type: "success"
});
displayNewClientCertificate(certificate);
onComplete();
};
return (
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl label="TTL" isError={Boolean(error)} errorText={error?.message} isRequired>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
helperText="This defines the key algorithm to use for signing the client certificate."
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Generate Client Certificate
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const CreateKmipClientCertificateModal = ({
isOpen,
onOpenChange,
kmipClient,
displayNewClientCertificate
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Generate KMIP client certificate">
<KmipClientCertificateForm
onComplete={() => onOpenChange(false)}
displayNewClientCertificate={displayNewClientCertificate}
kmipClient={kmipClient}
/>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,53 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { useDeleteKmipClients } from "@app/hooks/api/kmip";
import { TKmipClient } from "@app/hooks/api/kmip/types";
type Props = {
kmipClient: TKmipClient;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
const deleteKmipClients = useDeleteKmipClients();
if (!kmipClient) return null;
const { id, projectId, name } = kmipClient;
const handleDeleteKmipClient = async () => {
try {
await deleteKmipClients.mutateAsync({
id,
projectId
});
createNotification({
text: "KMIP client successfully deleted",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete KMIP client";
createNotification({
text,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteKmipClient}
/>
);
};

View File

@ -0,0 +1,19 @@
import { Modal, ModalContent } from "@app/components/v2";
import { KmipClientCertificate } from "@app/hooks/api/kmip/types";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
certificate: KmipClientCertificate;
};
export const KmipClientCertificateModal = ({ isOpen, onOpenChange, certificate }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="KMIP client certificate">
<CertificateContent {...certificate} />
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,194 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip";
import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types";
const KMIP_PERMISSIONS_OPTIONS = [
{ value: KmipPermission.Check, label: "Check" },
{ value: KmipPermission.Create, label: "Create" },
{ value: KmipPermission.Get, label: "Get" },
{ value: KmipPermission.Locate, label: "Locate" },
{ value: KmipPermission.Destroy, label: "Destroy" },
{ value: KmipPermission.Activate, label: "Activate" },
{ value: KmipPermission.Revoke, label: "Revoke" },
{ value: KmipPermission.GetAttributes, label: "Get Attributes" },
{ value: KmipPermission.Register, label: "Register" }
] as const;
const formSchema = z.object({
name: z.string().trim().min(1),
description: z.string().max(500).optional(),
permissions: z.object({
[KmipPermission.Check]: z.boolean().optional(),
[KmipPermission.Create]: z.boolean().optional(),
[KmipPermission.Get]: z.boolean().optional(),
[KmipPermission.Locate]: z.boolean().optional(),
[KmipPermission.Destroy]: z.boolean().optional(),
[KmipPermission.Activate]: z.boolean().optional(),
[KmipPermission.GetAttributes]: z.boolean().optional(),
[KmipPermission.Revoke]: z.boolean().optional(),
[KmipPermission.Register]: z.boolean().optional()
})
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
kmipClient?: TKmipClient | null;
};
type FormProps = Pick<Props, "kmipClient"> & {
onComplete: () => void;
};
const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
const createKmipClient = useCreateKmipClient();
const updateKmipClient = useUpdateKmipClient();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const isUpdate = !!kmipClient;
const {
control,
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: kmipClient?.name,
description: kmipClient?.description,
permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true]))
}
});
const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => {
const mutation = isUpdate
? updateKmipClient.mutateAsync({
id: kmipClient.id,
projectId,
name,
description,
permissions: Object.entries(permissions)
.filter(([, value]) => value)
.map(([key]) => key as KmipPermission)
})
: createKmipClient.mutateAsync({
projectId,
name,
description,
permissions: Object.entries(permissions)
.filter(([, value]) => value)
.map(([key]) => key as KmipPermission)
});
try {
await mutation;
createNotification({
text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
<FormControl
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="My KMIP Client" {...register("name")} />
</FormControl>
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
isError={Boolean(errors.description?.message)}
>
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
{...register("description")}
/>
</FormControl>
<Controller
control={control}
name="permissions"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl label="Permissions" errorText={error?.message} isError={Boolean(error)}>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KMIP_PERMISSIONS_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"} KMIP client
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const KmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={`${kmipClient ? "Update" : "Add"} KMIP Client`}>
<KmipClientForm onComplete={() => onOpenChange(false)} kmipClient={kmipClient} />
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,338 @@
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faCertificate,
faEdit,
faEllipsis,
faMagnifyingGlass,
faPlus,
faTrash,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
ProjectPermissionKmipActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip";
import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types";
import { CreateKmipClientCertificateModal } from "./CreateKmipClientCertificateModal";
import { DeleteKmipClientModal } from "./DeleteKmipClientModal";
import { KmipClientCertificateModal } from "./KmipClientCertificateModal";
import { KmipClientModal } from "./KmipClientModal";
export const KmipClientTable = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id ?? "";
const {
offset,
limit,
orderBy,
orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination(KmipClientOrderBy.Name);
const { data, isPending, isFetching } = useGetKmipClientsByProjectId({
projectId,
offset,
limit,
search: debouncedSearch,
orderBy,
orderDirection
});
const { permission } = useProjectPermission();
const { subscription } = useSubscription();
const { kmipClients = [], totalCount = 0 } = data ?? {};
useResetPageHelper({
totalCount,
offset,
setPage
});
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKmipClient",
"deleteKmipClient",
"generateKmipClientCert",
"displayKmipClientCert",
"upgradePlan"
] as const);
const handleSort = () => {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
};
const cannotEditKmipClient = permission.cannot(
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionSub.Kmip
);
const cannotDeleteKmipClient = permission.cannot(
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionSub.Kmip
);
const cannotGenerateKmipClientCertificate = permission.cannot(
ProjectPermissionKmipActions.GenerateClientCertificates,
ProjectPermissionSub.Kmip
);
return (
<motion.div
key="kmip-clients-tab"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="whitespace-nowrap text-xl font-semibold text-mineshaft-100">KMIP Clients</p>
<div className="flex w-full justify-end pr-4">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/kms"
>
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</a>
</div>
<ProjectPermissionCan
I={ProjectPermissionKmipActions.CreateClients}
a={ProjectPermissionSub.Kmip}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("upsertKmipClient", null);
}}
isDisabled={!isAllowed}
>
Add KMIP Client
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search clients by name..."
/>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={handleSort}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Description</Th>
<Th>Permissions</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="project-kmip-clients" />}
{!isPending &&
kmipClients.length > 0 &&
kmipClients.map((kmipClient) => {
const { name, id, description, permissions } = kmipClient;
return (
<Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}>
<Td>{name}</Td>
<Td className="max-w-80 break-all">{description}</Td>
<Td className="max-w-40">{[permissions.join(", ")]}</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
variant="plain"
colorSchema="primary"
className="ml-4 p-0 data-[state=open]:text-primary-400"
ariaLabel="More options"
>
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
cannotGenerateKmipClientCertificate ? "Access Restricted" : ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("generateKmipClientCert", kmipClient)
}
icon={<FontAwesomeIcon icon={faCertificate} />}
iconPos="left"
isDisabled={cannotGenerateKmipClientCertificate}
>
Generate Certificate
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKmipClient ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("upsertKmipClient", kmipClient)}
icon={<FontAwesomeIcon icon={faEdit} />}
iconPos="left"
isDisabled={cannotEditKmipClient}
>
Edit KMIP Client
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotDeleteKmipClient ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteKmipClient", kmipClient)}
icon={<FontAwesomeIcon icon={faTrash} />}
iconPos="left"
isDisabled={cannotDeleteKmipClient}
>
Delete KMIP Client
</DropdownMenuItem>
</div>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isPending && kmipClients.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No KMIP clients match search filter"
: "No KMIP clients have been added to this project"
}
icon={faUser}
/>
)}
</TableContainer>
<DeleteKmipClientModal
isOpen={popUp.deleteKmipClient.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteKmipClient", isOpen)}
kmipClient={popUp.deleteKmipClient.data as TKmipClient}
/>
<KmipClientModal
isOpen={popUp.upsertKmipClient.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)}
kmipClient={popUp.upsertKmipClient.data as TKmipClient | null}
/>
<CreateKmipClientCertificateModal
isOpen={popUp.generateKmipClientCert.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("generateKmipClientCert", isOpen)}
kmipClient={popUp.generateKmipClientCert.data as TKmipClient | null}
displayNewClientCertificate={(certificate) =>
handlePopUpOpen("displayKmipClientCert", certificate)
}
/>
<KmipClientCertificateModal
isOpen={popUp.displayKmipClientCert.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("displayKmipClientCert", isOpen)}
certificate={popUp.displayKmipClientCert.data}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="KMIP requires an enterprise plan."
/>
</div>
</motion.div>
);
};

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