secret sync + tweaks

This commit is contained in:
x032205
2025-05-07 17:57:00 -04:00
parent 581840a701
commit 36954a9df9
36 changed files with 799 additions and 28 deletions
+1 -1
View File
@@ -2084,7 +2084,7 @@ export const SecretSyncs = {
OCI_VAULT: {
compartmentOcid: "The OCID (Oracle Cloud Identifier) of the compartment where the vault is located.",
vaultOcid: "The OCID (Oracle Cloud Identifier) of the vault to sync secrets to.",
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault." // TODO(andrey): May or may not be needed. This refers to encryption key.
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
}
}
};
@@ -22,6 +22,34 @@ export const registerOCIConnectionRouter = async (server: FastifyZodProvider) =>
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/compartments`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const compartments = await server.services.appConnection.oci.listCompartments(connectionId, req.permission);
return compartments;
}
});
server.route({
method: "GET",
url: `/:connectionId/vaults`,
@@ -10,6 +10,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -31,5 +32,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter
[SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter
};
@@ -0,0 +1,17 @@
import {
CreateOCIVaultSyncSchema,
OCIVaultSyncSchema,
UpdateOCIVaultSyncSchema
} from "@app/services/secret-sync/oci-vault";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.OCIVault,
server,
responseSchema: OCIVaultSyncSchema,
createSchema: CreateOCIVaultSyncSchema,
updateSchema: UpdateOCIVaultSyncSchema
});
@@ -24,6 +24,7 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -43,7 +44,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
VercelSyncSchema,
WindmillSyncSchema,
HCVaultSyncSchema,
TeamCitySyncSchema
TeamCitySyncSchema,
OCIVaultSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -60,7 +62,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
VercelSyncListItemSchema,
WindmillSyncListItemSchema,
HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema
TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -1,6 +1,5 @@
import { common, identity, keymanagement, vault } from "oci-sdk";
import { common, identity, keymanagement } from "oci-sdk";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@@ -56,6 +55,30 @@ export const validateOCIConnectionCredentials = async (config: TOCIConnectionCon
return config.credentials;
};
export const listOCICompartments = async (appConnection: TOCIConnection) => {
const provider = await getOCIProvider(appConnection);
const identityClient = new identity.IdentityClient({ authenticationDetailsProvider: provider });
const rootCompartment = await identityClient
.getTenancy({
tenancyId: appConnection.credentials.tenancyOcid
})
.then((response) => ({
...response.tenancy,
id: appConnection.credentials.tenancyOcid,
name: response.tenancy.name ? `${response.tenancy.name} (root)` : "root"
}));
const compartments = await identityClient.listCompartments({
compartmentId: appConnection.credentials.tenancyOcid,
compartmentIdInSubtree: true,
accessLevel: identity.requests.ListCompartmentsRequest.AccessLevel.Any
});
return [rootCompartment, ...compartments.items];
};
export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOcid: string) => {
const provider = await getOCIProvider(appConnection);
@@ -1,7 +1,7 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
import { TOCIConnection } from "./oci-connection-types";
type TGetAppConnectionFunc = (
@@ -22,6 +22,17 @@ type TListOCIVaultKeysDTO = {
};
export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const compartments = await listOCICompartments(appConnection);
return compartments;
} catch (error) {
return [];
}
};
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
@@ -48,6 +59,7 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
};
return {
listCompartments,
listVaults,
listVaultKeys
};
@@ -0,0 +1,4 @@
export * from "./oci-vault-sync-constants";
export * from "./oci-vault-sync-fns";
export * from "./oci-vault-sync-schemas";
export * from "./oci-vault-sync-types";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const OCI_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "OCI Vault",
destination: SecretSync.OCIVault,
connection: AppConnection.OCI,
canImportSecrets: true
};
@@ -0,0 +1,218 @@
import { secrets, vault } from "oci-sdk";
import { getOCIProvider } from "@app/services/app-connection/oci";
import {
TCreateOCIVaultVariable,
TDeleteOCIVaultVariable,
TOCIVaultListVariables,
TOCIVaultSyncWithCredentials,
TUpdateOCIVaultVariable
} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listOCIVaultVariables = async ({ provider, compartmentId, vaultId, onlyActive }: TOCIVaultListVariables) => {
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
const secretsClient = new secrets.SecretsClient({ authenticationDetailsProvider: provider });
const secretsRes = await vaultsClient.listSecrets({
compartmentId,
vaultId,
lifecycleState: onlyActive ? vault.models.SecretSummary.LifecycleState.Active : undefined
});
const result: Record<string, vault.models.SecretSummary & { name: string; value: string }> = {};
for await (const s of secretsRes.items) {
let secretValue = "";
if (s.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
const secretBundle = await secretsClient.getSecretBundle({
secretId: s.id
});
secretValue = Buffer.from(secretBundle.secretBundle.secretBundleContent?.content || "", "base64").toString(
"utf-8"
);
}
result[s.secretName] = {
...s,
name: s.secretName,
value: secretValue
};
}
return result;
};
const createOCIVaultVariable = async ({
provider,
compartmentId,
vaultId,
keyId,
name,
value
}: TCreateOCIVaultVariable) => {
if (!value) return;
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
return vaultsClient.createSecret({
createSecretDetails: {
compartmentId,
vaultId,
keyId,
secretName: name,
enableAutoGeneration: false,
secretContent: {
content: Buffer.from(value).toString("base64"),
contentType: "BASE64"
}
}
});
};
const updateOCIVaultVariable = async ({ provider, secretId, value }: TUpdateOCIVaultVariable) => {
if (!value) return;
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
return vaultsClient.updateSecret({
secretId,
updateSecretDetails: {
enableAutoGeneration: false,
secretContent: {
content: Buffer.from(value).toString("base64"),
contentType: "BASE64"
}
}
});
};
const deleteOCIVaultVariable = async ({ provider, secretId }: TDeleteOCIVaultVariable) => {
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
// Schedule a secret deletion 7 days from now. OCI Vault requires a MINIMUM buffer period of 7 days
return vaultsClient.scheduleSecretDeletion({
secretId,
scheduleSecretDeletionDetails: {
timeOfDeletion: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
};
export const OCIVaultSyncFns = {
syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { compartmentOcid, vaultOcid, keyOcid }
} = secretSync;
const provider = await getOCIProvider(connection);
const variables = await listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid });
// Create secrets
for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry;
if (!Object.values(variables).some((v) => v.secretName === key)) {
try {
await createOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
keyId: keyOcid,
name: key,
value
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
// Update and delete secrets
for await (const [key, variable] of Object.entries(variables)) {
// Only update / delete active secrets
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
if (key in secretMap) {
if (variable.value !== secretMap[key].value) {
try {
await updateOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
secretId: variable.id,
value: secretMap[key].value
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
} else if (!secretSync.syncOptions.disableSecretDeletion) {
try {
await deleteOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
secretId: variable.id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
}
},
removeSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { compartmentOcid, vaultOcid }
} = secretSync;
const provider = await getOCIProvider(connection);
const variables = await listOCIVaultVariables({
provider,
compartmentId: compartmentOcid,
vaultId: vaultOcid,
onlyActive: true
});
for await (const [key, variable] of Object.entries(variables)) {
if (key in secretMap) {
try {
await deleteOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
secretId: variable.id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
getSecrets: async (secretSync: TOCIVaultSyncWithCredentials) => {
const {
connection,
destinationConfig: { compartmentOcid, vaultOcid }
} = secretSync;
const provider = await getOCIProvider(connection);
return listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid, onlyActive: true });
}
};
@@ -0,0 +1,53 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const OCIVaultSyncDestinationConfigSchema = z.object({
compartmentOcid: z
.string()
.trim()
.min(1, "Compartment OCID required")
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.compartmentOcid),
vaultOcid: z
.string()
.trim()
.min(1, "Vault OCID required")
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.vaultOcid),
keyOcid: z.string().trim().min(1, "Key OCID required").describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.keyOcid)
});
const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig).extend({
destination: z.literal(SecretSync.OCIVault),
destinationConfig: OCIVaultSyncDestinationConfigSchema
});
export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.OCIVault,
OCIVaultSyncOptionsConfig
).extend({
destinationConfig: OCIVaultSyncDestinationConfigSchema
});
export const UpdateOCIVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.OCIVault,
OCIVaultSyncOptionsConfig
).extend({
destinationConfig: OCIVaultSyncDestinationConfigSchema.optional()
});
export const OCIVaultSyncListItemSchema = z.object({
name: z.literal("OCI Vault"),
connection: z.literal(AppConnection.OCI),
destination: z.literal(SecretSync.OCIVault),
canImportSecrets: z.literal(true)
});
@@ -0,0 +1,44 @@
import { SimpleAuthenticationDetailsProvider } from "oci-sdk";
import { z } from "zod";
import { TOCIConnection } from "@app/services/app-connection/oci";
import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas";
export type TOCIVaultSync = z.infer<typeof OCIVaultSyncSchema>;
export type TOCIVaultSyncInput = z.infer<typeof CreateOCIVaultSyncSchema>;
export type TOCIVaultSyncListItem = z.infer<typeof OCIVaultSyncListItemSchema>;
export type TOCIVaultSyncWithCredentials = TOCIVaultSync & {
connection: TOCIConnection;
};
export type TOCIVaultVariable = {
id: string;
name: string;
value: string;
};
export type TOCIVaultListVariables = {
provider: SimpleAuthenticationDetailsProvider;
compartmentId: string;
vaultId: string;
onlyActive?: boolean; // Whether to filter for only active secrets. Removes deleted / scheduled for deletion secrets
};
export type TCreateOCIVaultVariable = TOCIVaultListVariables & {
keyId: string;
name: string;
value: string;
};
export type TUpdateOCIVaultVariable = TOCIVaultListVariables & {
secretId: string;
value: string;
};
export type TDeleteOCIVaultVariable = TOCIVaultListVariables & {
secretId: string;
};
@@ -12,7 +12,8 @@ export enum SecretSync {
Vercel = "vercel",
Windmill = "windmill",
HCVault = "hashicorp-vault",
TeamCity = "teamcity"
TeamCity = "teamcity",
OCIVault = "oci-vault"
}
export enum SecretSyncInitialSyncBehavior {
@@ -28,6 +28,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "./oci-vault";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
@@ -47,7 +48,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
[SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION,
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION,
[SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -148,6 +150,8 @@ export const SecretSyncFns = {
return HCVaultSyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -213,6 +217,9 @@ export const SecretSyncFns = {
case SecretSync.TeamCity:
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
break;
case SecretSync.OCIVault:
secretMap = await OCIVaultSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -270,6 +277,8 @@ export const SecretSyncFns = {
return HCVaultSyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, secretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -15,7 +15,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Vercel]: "Vercel",
[SecretSync.Windmill]: "Windmill",
[SecretSync.HCVault]: "Hashicorp Vault",
[SecretSync.TeamCity]: "TeamCity"
[SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -32,5 +33,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault,
[SecretSync.TeamCity]: AppConnection.TeamCity
[SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI
};
@@ -67,6 +67,7 @@ import {
THumanitecSyncListItem,
THumanitecSyncWithCredentials
} from "./humanitec";
import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault";
import {
TTeamCitySync,
TTeamCitySyncInput,
@@ -95,7 +96,8 @@ export type TSecretSync =
| TVercelSync
| TWindmillSync
| THCVaultSync
| TTeamCitySync;
| TTeamCitySync
| TOCIVaultSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -111,7 +113,8 @@ export type TSecretSyncWithCredentials =
| TVercelSyncWithCredentials
| TWindmillSyncWithCredentials
| THCVaultSyncWithCredentials
| TTeamCitySyncWithCredentials;
| TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -127,7 +130,8 @@ export type TSecretSyncInput =
| TVercelSyncInput
| TWindmillSyncInput
| THCVaultSyncInput
| TTeamCitySyncInput;
| TTeamCitySyncInput
| TOCIVaultSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -143,7 +147,8 @@ export type TSecretSyncListItem =
| TVercelSyncListItem
| TWindmillSyncListItem
| THCVaultSyncListItem
| TTeamCitySyncListItem;
| TTeamCitySyncListItem
| TOCIVaultSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;