mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 18:41:21 +00:00
Merge
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import z from "zod";
|
||||
|
||||
import {
|
||||
CreateOCIConnectionSchema,
|
||||
SanitizedOCIConnectionSchema,
|
||||
UpdateOCIConnectionSchema
|
||||
} from "@app/ee/services/app-connections/oci";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "../../../../server/routes/v1/app-connection-routers/app-connection-endpoints";
|
||||
|
||||
export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.OCI,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedOCIConnectionSchema,
|
||||
createSchema: CreateOCIConnectionSchema,
|
||||
updateSchema: UpdateOCIConnectionSchema
|
||||
});
|
||||
|
||||
// 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`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
compartmentOcid: z.string().min(1, "Compartment OCID required")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
displayName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { compartmentOcid } = req.query;
|
||||
|
||||
const vaults = await server.services.appConnection.oci.listVaults(
|
||||
{ connectionId, compartmentOcid },
|
||||
req.permission
|
||||
);
|
||||
return vaults;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/vault-keys`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
compartmentOcid: z.string().min(1, "Compartment OCID required"),
|
||||
vaultOcid: z.string().min(1, "Vault OCID required")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
displayName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { compartmentOcid, vaultOcid } = req.query;
|
||||
|
||||
const keys = await server.services.appConnection.oci.listVaultKeys(
|
||||
{ connectionId, compartmentOcid, vaultOcid },
|
||||
req.permission
|
||||
);
|
||||
return keys;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
CreateOCIVaultSyncSchema,
|
||||
OCIVaultSyncSchema,
|
||||
UpdateOCIVaultSyncSchema
|
||||
} from "@app/ee/services/secret-sync/oci-vault";
|
||||
import { registerSyncSecretsEndpoints } from "@app/server/routes/v1/secret-sync-routers/secret-sync-endpoints";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.OCIVault,
|
||||
server,
|
||||
responseSchema: OCIVaultSyncSchema,
|
||||
createSchema: CreateOCIVaultSyncSchema,
|
||||
updateSchema: UpdateOCIVaultSyncSchema
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./oci-connection-enums";
|
||||
export * from "./oci-connection-fns";
|
||||
export * from "./oci-connection-schemas";
|
||||
export * from "./oci-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum OCIConnectionMethod {
|
||||
AccessKey = "access-key"
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { common, identity, keymanagement } from "oci-sdk";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { OCIConnectionMethod } from "./oci-connection-enums";
|
||||
import { TOCIConnection, TOCIConnectionConfig } from "./oci-connection-types";
|
||||
|
||||
export const getOCIProvider = async (config: TOCIConnectionConfig) => {
|
||||
const {
|
||||
credentials: { fingerprint, privateKey, region, tenancyOcid, userOcid }
|
||||
} = config;
|
||||
|
||||
const provider = new common.SimpleAuthenticationDetailsProvider(
|
||||
tenancyOcid,
|
||||
userOcid,
|
||||
fingerprint,
|
||||
privateKey,
|
||||
null,
|
||||
common.Region.fromRegionId(region)
|
||||
);
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
export const getOCIConnectionListItem = () => {
|
||||
return {
|
||||
name: "OCI" as const,
|
||||
app: AppConnection.OCI as const,
|
||||
methods: Object.values(OCIConnectionMethod) as [OCIConnectionMethod.AccessKey]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateOCIConnectionCredentials = async (config: TOCIConnectionConfig) => {
|
||||
const provider = await getOCIProvider(config);
|
||||
|
||||
try {
|
||||
const identityClient = new identity.IdentityClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
// Get user details - a lightweight call that validates all credentials
|
||||
await identityClient.getUser({ userId: config.credentials.userOcid });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
|
||||
export const listOCICompartments = async (appConnection: TOCIConnection) => {
|
||||
const provider = await getOCIProvider(appConnection);
|
||||
|
||||
const identityClient = new identity.IdentityClient({ authenticationDetailsProvider: provider });
|
||||
const keyManagementClient = new keymanagement.KmsVaultClient({
|
||||
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,
|
||||
lifecycleState: identity.models.Compartment.LifecycleState.Active
|
||||
});
|
||||
|
||||
const allCompartments = [rootCompartment, ...compartments.items];
|
||||
const filteredCompartments = [];
|
||||
|
||||
for await (const compartment of allCompartments) {
|
||||
try {
|
||||
// Check if user can list vaults in this compartment
|
||||
await keyManagementClient.listVaults({
|
||||
compartmentId: compartment.id,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
filteredCompartments.push(compartment);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCompartments;
|
||||
};
|
||||
|
||||
export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOcid: string) => {
|
||||
const provider = await getOCIProvider(appConnection);
|
||||
|
||||
const keyManagementClient = new keymanagement.KmsVaultClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
const vaults = await keyManagementClient.listVaults({
|
||||
compartmentId: compartmentOcid
|
||||
});
|
||||
|
||||
return vaults.items.filter((v) => v.lifecycleState === keymanagement.models.Vault.LifecycleState.Active);
|
||||
};
|
||||
|
||||
export const listOCIVaultKeys = async (appConnection: TOCIConnection, compartmentOcid: string, vaultOcid: string) => {
|
||||
const provider = await getOCIProvider(appConnection);
|
||||
|
||||
const kmsVaultClient = new keymanagement.KmsVaultClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
const vault = await kmsVaultClient.getVault({
|
||||
vaultId: vaultOcid
|
||||
});
|
||||
|
||||
const keyManagementClient = new keymanagement.KmsManagementClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
keyManagementClient.endpoint = vault.vault.managementEndpoint;
|
||||
|
||||
const keys = await keyManagementClient.listKeys({
|
||||
compartmentId: compartmentOcid
|
||||
});
|
||||
|
||||
return keys.items.filter((v) => v.lifecycleState === keymanagement.models.KeySummary.LifecycleState.Enabled);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { OCIConnectionMethod } from "./oci-connection-enums";
|
||||
|
||||
export const OCIConnectionAccessTokenCredentialsSchema = z.object({
|
||||
userOcid: z.string().trim().min(1, "User OCID required").describe(AppConnections.CREDENTIALS.OCI.userOcid),
|
||||
tenancyOcid: z.string().trim().min(1, "Tenancy OCID required").describe(AppConnections.CREDENTIALS.OCI.tenancyOcid),
|
||||
region: z.string().trim().min(1, "Region required").describe(AppConnections.CREDENTIALS.OCI.region),
|
||||
fingerprint: z.string().trim().min(1, "Fingerprint required").describe(AppConnections.CREDENTIALS.OCI.fingerprint),
|
||||
privateKey: z.string().trim().min(1, "Private Key required").describe(AppConnections.CREDENTIALS.OCI.privateKey)
|
||||
});
|
||||
|
||||
const BaseOCIConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OCI) });
|
||||
|
||||
export const OCIConnectionSchema = BaseOCIConnectionSchema.extend({
|
||||
method: z.literal(OCIConnectionMethod.AccessKey),
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseOCIConnectionSchema.extend({
|
||||
method: z.literal(OCIConnectionMethod.AccessKey),
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema.pick({
|
||||
userOcid: true,
|
||||
tenancyOcid: true,
|
||||
region: true,
|
||||
fingerprint: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(OCIConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.OCI).method),
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.OCI).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateOCIConnectionSchema = ValidateOCIConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.OCI)
|
||||
);
|
||||
|
||||
export const UpdateOCIConnectionSchema = z
|
||||
.object({
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.OCI).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI));
|
||||
|
||||
export const OCIConnectionListItemSchema = z.object({
|
||||
name: z.literal("OCI"),
|
||||
app: z.literal(AppConnection.OCI),
|
||||
methods: z.nativeEnum(OCIConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
|
||||
import { TLicenseServiceFactory } from "../../license/license-service";
|
||||
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
|
||||
import { TOCIConnection } from "./oci-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TOCIConnection>;
|
||||
|
||||
type TListOCIVaultsDTO = {
|
||||
connectionId: string;
|
||||
compartmentOcid: string;
|
||||
};
|
||||
|
||||
type TListOCIVaultKeysDTO = {
|
||||
connectionId: string;
|
||||
compartmentOcid: string;
|
||||
vaultOcid: string;
|
||||
};
|
||||
|
||||
// Enterprise check
|
||||
export const checkPlan = async (licenseService: Pick<TLicenseServiceFactory, "getPlan">, orgId: string) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.enterpriseAppConnections)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||
});
|
||||
};
|
||||
|
||||
export const ociConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">
|
||||
) => {
|
||||
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
await checkPlan(licenseService, actor.orgId);
|
||||
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
const compartments = await listOCICompartments(appConnection);
|
||||
return compartments;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with OCI");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
|
||||
await checkPlan(licenseService, actor.orgId);
|
||||
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
const vaults = await listOCIVaults(appConnection, compartmentOcid);
|
||||
return vaults;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with OCI");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listVaultKeys = async (
|
||||
{ connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
await checkPlan(licenseService, actor.orgId);
|
||||
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
const keys = await listOCIVaultKeys(appConnection, compartmentOcid, vaultOcid);
|
||||
return keys;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with OCI");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listCompartments,
|
||||
listVaults,
|
||||
listVaultKeys
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateOCIConnectionSchema,
|
||||
OCIConnectionSchema,
|
||||
ValidateOCIConnectionCredentialsSchema
|
||||
} from "./oci-connection-schemas";
|
||||
|
||||
export type TOCIConnection = z.infer<typeof OCIConnectionSchema>;
|
||||
|
||||
export type TOCIConnectionInput = z.infer<typeof CreateOCIConnectionSchema> & {
|
||||
app: AppConnection.OCI;
|
||||
};
|
||||
|
||||
export type TValidateOCIConnectionCredentialsSchema = typeof ValidateOCIConnectionCredentialsSchema;
|
||||
|
||||
export type TOCIConnectionConfig = DiscriminativePick<TOCIConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
@@ -29,7 +29,9 @@ export const getDefaultOnPremFeatures = () => {
|
||||
secretApproval: true,
|
||||
secretRotation: true,
|
||||
caCrl: false,
|
||||
sshHostGroups: false
|
||||
sshHostGroups: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseAppConnections: false
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.Users}.isGhost`, false)
|
||||
.count();
|
||||
return Number(doc?.[0].count);
|
||||
return Number(doc?.[0]?.count ?? 0);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count of Org Members" });
|
||||
}
|
||||
|
||||
@@ -18,33 +18,33 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
environmentsUsed: 0,
|
||||
identityLimit: null,
|
||||
identitiesUsed: 0,
|
||||
dynamicSecret: true,
|
||||
dynamicSecret: false,
|
||||
secretVersioning: true,
|
||||
pitRecovery: true,
|
||||
ipAllowlisting: true,
|
||||
rbac: true,
|
||||
githubOrgSync: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: false,
|
||||
githubOrgSync: false,
|
||||
customRateLimits: false,
|
||||
customAlerts: true,
|
||||
secretAccessInsights: true,
|
||||
auditLogs: true,
|
||||
auditLogsRetentionDays: 3,
|
||||
auditLogStreams: true,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: true,
|
||||
hsm: true,
|
||||
oidcSSO: true,
|
||||
scim: true,
|
||||
ldap: true,
|
||||
groups: true,
|
||||
samlSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
ldap: false,
|
||||
groups: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: true,
|
||||
secretRotation: true,
|
||||
caCrl: true,
|
||||
instanceUserManagement: true,
|
||||
externalKms: true,
|
||||
secretApproval: false,
|
||||
secretRotation: false,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false,
|
||||
rateLimits: {
|
||||
readLimit: 60,
|
||||
writeLimit: 200,
|
||||
@@ -52,10 +52,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
},
|
||||
pkiEst: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: true,
|
||||
kmip: true,
|
||||
gateway: true,
|
||||
sshHostGroups: true
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false,
|
||||
sshHostGroups: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseAppConnections: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
||||
@@ -92,6 +92,10 @@ export const licenseServiceFactory = ({
|
||||
const {
|
||||
data: { currentPlan }
|
||||
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
|
||||
|
||||
const workspacesUsed = await projectDAL.countOfOrgProjects(null);
|
||||
currentPlan.workspacesUsed = workspacesUsed;
|
||||
|
||||
onPremFeatures = currentPlan;
|
||||
logger.info("Successfully synchronized license key features");
|
||||
} catch (error) {
|
||||
@@ -185,6 +189,9 @@ export const licenseServiceFactory = ({
|
||||
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`
|
||||
);
|
||||
const workspacesUsed = await projectDAL.countOfOrgProjects(orgId);
|
||||
currentPlan.workspacesUsed = workspacesUsed;
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
FEATURE_CACHE_KEY(org.id),
|
||||
LICENSE_SERVER_CLOUD_PLAN_TTL,
|
||||
|
||||
@@ -27,7 +27,7 @@ export type TFeatureSet = {
|
||||
slug: null;
|
||||
tier: -1;
|
||||
workspaceLimit: null;
|
||||
workspacesUsed: 0;
|
||||
workspacesUsed: number;
|
||||
dynamicSecret: false;
|
||||
memberLimit: null;
|
||||
membersUsed: number;
|
||||
@@ -72,6 +72,8 @@ export type TFeatureSet = {
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
sshHostGroups: false;
|
||||
enterpriseSecretSyncs: false;
|
||||
enterpriseAppConnections: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
||||
@@ -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,11 @@
|
||||
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,
|
||||
enterprise: true
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
import { secrets, vault } from "oci-sdk";
|
||||
|
||||
import { getOCIProvider } from "@app/ee/services/app-connections/oci";
|
||||
import {
|
||||
TCreateOCIVaultVariable,
|
||||
TDeleteOCIVaultVariable,
|
||||
TOCIVaultListVariables,
|
||||
TOCIVaultSyncWithCredentials,
|
||||
TUnmarkOCIVaultVariableFromDeletion,
|
||||
TUpdateOCIVaultVariable
|
||||
} from "@app/ee/services/secret-sync/oci-vault/oci-vault-sync-types";
|
||||
import { delay } from "@app/lib/delay";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
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)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const unmarkOCIVaultVariableFromDeletion = async ({ provider, secretId }: TUnmarkOCIVaultVariableFromDeletion) => {
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
return vaultsClient.cancelSecretDeletion({
|
||||
secretId
|
||||
});
|
||||
};
|
||||
|
||||
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 });
|
||||
|
||||
// Throw an error if any keys are updating in OCI vault to prevent skipped updates
|
||||
if (
|
||||
Object.entries(variables).some(
|
||||
([, secret]) =>
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Updating ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.CancellingDeletion ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Creating ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Deleting ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.SchedulingDeletion
|
||||
)
|
||||
) {
|
||||
throw new SecretSyncError({
|
||||
error: "Cannot sync while keys are updating in OCI Vault."
|
||||
});
|
||||
}
|
||||
|
||||
// Create secrets
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingVariable = Object.values(variables).find((v) => v.secretName === key);
|
||||
|
||||
if (!existingVariable) {
|
||||
try {
|
||||
await createOCIVaultVariable({
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
provider,
|
||||
keyId: keyOcid,
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else if (existingVariable.lifecycleState === vault.models.SecretSummary.LifecycleState.PendingDeletion) {
|
||||
// If a secret exists but is pending deletion, cancel the deletion and update the secret
|
||||
await unmarkOCIVaultVariableFromDeletion({
|
||||
provider,
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
secretId: existingVariable.id
|
||||
});
|
||||
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
const MAX_RETRIES = 10;
|
||||
|
||||
for (let i = 0; i < MAX_RETRIES; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delay(5000);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const secret = await vaultsClient.getSecret({
|
||||
secretId: existingVariable.id
|
||||
});
|
||||
|
||||
if (secret.secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateOCIVaultVariable({
|
||||
provider,
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
secretId: existingVariable.id,
|
||||
value
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (i === MAX_RETRIES - 1) {
|
||||
throw new SecretSyncError({
|
||||
error: "Failed to update secret after cancelling deletion.",
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update and delete secrets
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
|
||||
|
||||
// Only update / delete active secrets
|
||||
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
|
||||
if (key in secretMap && secretMap[key].value.length > 0) {
|
||||
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,71 @@
|
||||
import RE2 from "re2";
|
||||
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")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.(tenancy|compartment)\\.oc1\\..+$").test(val),
|
||||
"Invalid Compartment OCID format. Must start with ocid1.tenancy.oc1. or ocid1.compartment.oc1."
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.compartmentOcid),
|
||||
vaultOcid: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Vault OCID required")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.vault\\.oc1\\..+$").test(val),
|
||||
"Invalid Vault OCID format. Must start with ocid1.vault.oc1."
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.vaultOcid),
|
||||
keyOcid: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Key OCID required")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.key\\.oc1\\..+$").test(val),
|
||||
"Invalid Key OCID format. Must start with ocid1.key.oc1."
|
||||
)
|
||||
.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),
|
||||
enterprise: z.boolean()
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { SimpleAuthenticationDetailsProvider } from "oci-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TOCIConnection } from "@app/ee/services/app-connections/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;
|
||||
};
|
||||
|
||||
export type TUnmarkOCIVaultVariableFromDeletion = TOCIVaultListVariables & {
|
||||
secretId: string;
|
||||
};
|
||||
Reference in New Issue
Block a user