Merge remote-tracking branch 'origin/main' into feat/acme-and-external-ca

This commit is contained in:
Sheen Capadngan
2025-05-19 20:11:48 +08:00
292 changed files with 9372 additions and 1110 deletions
+1941
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -38,8 +38,8 @@
"build:frontend": "npm run build --prefix ../frontend", "build:frontend": "npm run build --prefix ../frontend",
"start": "node --enable-source-maps dist/main.mjs", "start": "node --enable-source-maps dist/main.mjs",
"type:check": "tsc --noEmit", "type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src", "lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'", "lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
"test:unit": "vitest run -c vitest.unit.config.ts", "test:unit": "vitest run -c vitest.unit.config.ts",
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1", "test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
@@ -211,6 +211,7 @@
"mysql2": "^3.9.8", "mysql2": "^3.9.8",
"nanoid": "^3.3.8", "nanoid": "^3.3.8",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",
"odbc": "^2.4.9", "odbc": "^2.4.9",
"openid-client": "^5.6.5", "openid-client": "^5.6.5",
"ora": "^7.0.1", "ora": "^7.0.1",
+2
View File
@@ -69,6 +69,7 @@ import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service"; import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
@@ -210,6 +211,7 @@ declare module "fastify" {
identityGcpAuth: TIdentityGcpAuthServiceFactory; identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory; identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory; identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOciAuth: TIdentityOciAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory; identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory; identityJwtAuth: TIdentityJwtAuthServiceFactory;
identityLdapAuth: TIdentityLdapAuthServiceFactory; identityLdapAuth: TIdentityLdapAuthServiceFactory;
+8
View File
@@ -122,6 +122,9 @@ import {
TIdentityMetadata, TIdentityMetadata,
TIdentityMetadataInsert, TIdentityMetadataInsert,
TIdentityMetadataUpdate, TIdentityMetadataUpdate,
TIdentityOciAuths,
TIdentityOciAuthsInsert,
TIdentityOciAuthsUpdate,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOidcAuthsInsert, TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate, TIdentityOidcAuthsUpdate,
@@ -754,6 +757,11 @@ declare module "knex/types/tables" {
TIdentityAzureAuthsInsert, TIdentityAzureAuthsInsert,
TIdentityAzureAuthsUpdate TIdentityAzureAuthsUpdate
>; >;
[TableName.IdentityOciAuth]: KnexOriginal.CompositeTableType<
TIdentityOciAuths,
TIdentityOciAuthsInsert,
TIdentityOciAuthsUpdate
>;
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType< [TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOidcAuthsInsert, TIdentityOidcAuthsInsert,
@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityOciAuth))) {
await knex.schema.createTable(TableName.IdentityOciAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("type").notNullable();
t.string("tenancyOcid").notNullable();
t.string("allowedUsernames").nullable();
});
}
await createOnUpdateTrigger(knex, TableName.IdentityOciAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityOciAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityOciAuth);
}
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (!hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}
@@ -0,0 +1,110 @@
import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TableName } from "../schemas";
import { getMigrationEnvConfig } from "./utils/env-config";
import { getMigrationEncryptionServices } from "./utils/services";
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
// eslint-disable-next-line no-param-reassign
knex.replicaNode = () => {
return knex;
};
if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"))) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
table.index("gatewayId");
});
const existingDynamicSecretsWithProjectGatewayId = await knex(TableName.DynamicSecret)
.select(selectAllTableCols(TableName.DynamicSecret))
.whereNotNull(`${TableName.DynamicSecret}.projectGatewayId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.id`, `${TableName.DynamicSecret}.projectGatewayId`)
.whereNotNull(`${TableName.ProjectGateway}.gatewayId`)
.select(
knex.ref("projectId").withSchema(TableName.ProjectGateway).as("projectId"),
knex.ref("gatewayId").withSchema(TableName.ProjectGateway).as("projectGatewayGatewayId")
);
initLogger();
const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore();
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
const updatedDynamicSecrets = await Promise.all(
existingDynamicSecretsWithProjectGatewayId.map(async (existingDynamicSecret) => {
if (!existingDynamicSecret.projectGatewayGatewayId) {
const result = {
...existingDynamicSecret,
gatewayId: null
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
let decryptedStoredInput = JSON.parse(
secretManagerDecryptor({ cipherTextBlob: Buffer.from(existingDynamicSecret.encryptedInput) }).toString()
) as object;
// We're not removing the existing projectGatewayId from the input so we can easily rollback without having to re-encrypt the input
decryptedStoredInput = {
...decryptedStoredInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const encryptedInput = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(decryptedStoredInput))
}).cipherTextBlob;
const result = {
...existingDynamicSecret,
encryptedInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
})
);
for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.DynamicSecret)
.insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE))
.onConflict("id")
.merge();
}
}
}
export async function down(knex: Knex): Promise<void> {
// no re-encryption needed as we keep the old projectGatewayId in the input
if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId")) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}
@@ -0,0 +1,53 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const columns = await knex.table(TableName.Organization).columnInfo();
await knex.schema.alterTable(TableName.Organization, (t) => {
if (!columns.secretsProductEnabled) {
t.boolean("secretsProductEnabled").defaultTo(true);
}
if (!columns.pkiProductEnabled) {
t.boolean("pkiProductEnabled").defaultTo(true);
}
if (!columns.kmsProductEnabled) {
t.boolean("kmsProductEnabled").defaultTo(true);
}
if (!columns.sshProductEnabled) {
t.boolean("sshProductEnabled").defaultTo(true);
}
if (!columns.scannerProductEnabled) {
t.boolean("scannerProductEnabled").defaultTo(true);
}
if (!columns.shareSecretsProductEnabled) {
t.boolean("shareSecretsProductEnabled").defaultTo(true);
}
});
}
export async function down(knex: Knex): Promise<void> {
const columns = await knex.table(TableName.Organization).columnInfo();
await knex.schema.alterTable(TableName.Organization, (t) => {
if (columns.secretsProductEnabled) {
t.dropColumn("secretsProductEnabled");
}
if (columns.pkiProductEnabled) {
t.dropColumn("pkiProductEnabled");
}
if (columns.kmsProductEnabled) {
t.dropColumn("kmsProductEnabled");
}
if (columns.sshProductEnabled) {
t.dropColumn("sshProductEnabled");
}
if (columns.scannerProductEnabled) {
t.dropColumn("scannerProductEnabled");
}
if (columns.shareSecretsProductEnabled) {
t.dropColumn("shareSecretsProductEnabled");
}
});
}
@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSecretSharingColumn = await knex.schema.hasColumn(TableName.Project, "secretSharing");
if (!hasSecretSharingColumn) {
await knex.schema.table(TableName.Project, (table) => {
table.boolean("secretSharing").notNullable().defaultTo(true);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSecretSharingColumn = await knex.schema.hasColumn(TableName.Project, "secretSharing");
if (hasSecretSharingColumn) {
await knex.schema.table(TableName.Project, (table) => {
table.dropColumn("secretSharing");
});
}
}
+2 -1
View File
@@ -27,7 +27,8 @@ export const DynamicSecretsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
encryptedInput: zodBuffer, encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional() projectGatewayId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
@@ -29,7 +29,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
allowedNames: z.string(), allowedNames: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>; export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
@@ -0,0 +1,26 @@
// 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 IdentityOciAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
type: z.string(),
tenancyOcid: z.string(),
allowedUsernames: z.string().nullable().optional()
});
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
export type TIdentityOciAuthsInsert = Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>;
export type TIdentityOciAuthsUpdate = Partial<Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>>;
+1
View File
@@ -38,6 +38,7 @@ export * from "./identity-gcp-auths";
export * from "./identity-jwt-auths"; export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths"; export * from "./identity-kubernetes-auths";
export * from "./identity-metadata"; export * from "./identity-metadata";
export * from "./identity-oci-auths";
export * from "./identity-oidc-auths"; export * from "./identity-oidc-auths";
export * from "./identity-org-memberships"; export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege"; export * from "./identity-project-additional-privilege";
+2
View File
@@ -81,6 +81,7 @@ export enum TableName {
IdentityAzureAuth = "identity_azure_auths", IdentityAzureAuth = "identity_azure_auths",
IdentityUaClientSecret = "identity_ua_client_secrets", IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths", IdentityAwsAuth = "identity_aws_auths",
IdentityOciAuth = "identity_oci_auths",
IdentityOidcAuth = "identity_oidc_auths", IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths", IdentityJwtAuth = "identity_jwt_auths",
IdentityLdapAuth = "identity_ldap_auths", IdentityLdapAuth = "identity_ldap_auths",
@@ -235,6 +236,7 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth", GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth", AZURE_AUTH = "azure-auth",
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth", OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth", JWT_AUTH = "jwt-auth",
LDAP_AUTH = "ldap-auth" LDAP_AUTH = "ldap-auth"
+7 -1
View File
@@ -28,7 +28,13 @@ export const OrganizationsSchema = z.object({
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(), privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false), bypassOrgAuthEnabled: z.boolean().default(false),
userTokenExpiration: z.string().nullable().optional() userTokenExpiration: z.string().nullable().optional(),
secretsProductEnabled: z.boolean().default(true).nullable().optional(),
pkiProductEnabled: z.boolean().default(true).nullable().optional(),
kmsProductEnabled: z.boolean().default(true).nullable().optional(),
sshProductEnabled: z.boolean().default(true).nullable().optional(),
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional()
}); });
export type TOrganizations = z.infer<typeof OrganizationsSchema>; export type TOrganizations = z.infer<typeof OrganizationsSchema>;
+2 -1
View File
@@ -27,7 +27,8 @@ export const ProjectsSchema = z.object({
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
type: z.string(), type: z.string(),
enforceCapitalization: z.boolean().default(false), enforceCapitalization: z.boolean().default(false),
hasDeleteProtection: z.boolean().default(false).nullable().optional() hasDeleteProtection: z.boolean().default(false).nullable().optional(),
secretSharing: z.boolean().default(true)
}); });
export type TProjects = z.infer<typeof ProjectsSchema>; export type TProjects = z.infer<typeof ProjectsSchema>;
+6 -17
View File
@@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
}).array() }).array()
}) })
} }
@@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projectGatewayId: z.string()
}).array() }).array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({ const gateways = await server.services.gateway.listGateways({
projectId: req.params.projectId, orgPermission: req.permission
projectPermission: req.permission
}); });
return { gateways }; return { gateways };
} }
@@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
name: slugSchema({ field: "name" }).optional(), name: slugSchema({ field: "name" }).optional()
projectIds: z.string().array().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({ const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission, orgPermission: req.permission,
id: req.params.id, id: req.params.id,
name: req.body.name, name: req.body.name
projectIds: req.body.projectIds
}); });
return { gateway }; return { gateway };
} }
@@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds) allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
}) })
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), { .refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
message: "Max TLL must be greater than or equal to TTL", message: "Max TTL must be greater than or equal to TTL",
path: ["maxTTL"] path: ["maxTTL"]
}), }),
response: { response: {
@@ -162,6 +162,12 @@ export enum EventType {
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth", REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth", GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth",
ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth",
UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth",
REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth",
GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth",
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth", LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth", ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth", UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
@@ -1012,6 +1018,55 @@ interface GetIdentityAwsAuthEvent {
}; };
} }
interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
identityOciAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityOciAuthEvent {
type: EventType.ADD_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityOciAuthEvent {
type: EventType.REVOKE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityOciAuthEvent {
type: EventType.UPDATE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid?: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityOciAuthEvent {
type: EventType.GET_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityAzureAuthEvent { interface LoginIdentityAzureAuthEvent {
type: EventType.LOGIN_IDENTITY_AZURE_AUTH; type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
metadata: { metadata: {
@@ -2943,6 +2998,11 @@ export type Event =
| UpdateIdentityAwsAuthEvent | UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent | GetIdentityAwsAuthEvent
| DeleteIdentityAwsAuthEvent | DeleteIdentityAwsAuthEvent
| LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent
| GetIdentityOciAuthEvent
| DeleteIdentityOciAuthEvent
| LoginIdentityAzureAuthEvent | LoginIdentityAzureAuthEvent
| AddIdentityAzureAuthEvent | AddIdentityAzureAuthEvent
| DeleteIdentityAzureAuthEvent | DeleteIdentityAzureAuthEvent
@@ -17,7 +17,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal"; import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import {
DynamicSecretStatus, DynamicSecretStatus,
@@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">; gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
@@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
@@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs); const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) { if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const projectGatewayId = inputs.projectGatewayId as string; const gatewayId = inputs.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!projectGateway)
if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
name, name,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );
@@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if ( if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
updatedInput && const gatewayId = updatedInput.gatewayId as string;
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!projectGateway) if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
@@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
status: null, status: null,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );
@@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp"; import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = { type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const buildDynamicSecretProviders = ({ export const buildDynamicSecretProviders = ({
@@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
ca: z.string().optional(), ca: z.string().optional(),
projectGatewayId: z.string().nullable().optional() gatewayId: z.string().nullable().optional()
}); });
export const DynamicSecretCassandraSchema = z.object({ export const DynamicSecretCassandraSchema = z.object({
@@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
}; };
type TSqlDatabaseProviderDTO = { type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
}); });
@@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>, providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void> gatewayCallback: (host: string, port: number) => Promise<void>
) => { ) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string); const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy( await withGatewayProxy(
async (port) => { async (port) => {
@@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
+24 -59
View File
@@ -1,37 +1,34 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>; export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
export const gatewayDALFactory = (db: TDbClient) => { export const gatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Gateway); const orm = ormify(db, TableName.Gateway);
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => { const find = async (
filter: TFindFilter<TGateways> & { orgId?: string },
{ offset, limit, sort, tx }: TFindOpt<TGateways> = {}
) => {
try { try {
const query = (tx || db)(TableName.Gateway) const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter)) .where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) .join(
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`) TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Gateway}.identityId`
)
.select(selectAllTableCols(TableName.Gateway)) .select(selectAllTableCols(TableName.Gateway))
.select( .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
db.ref("name").withSchema(TableName.Identity).as("identityName"), .select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug"), if (filter.orgId) {
db.ref("id").withSchema(TableName.Project).as("projectId") void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId);
); }
if (limit) void query.limit(limit); if (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
@@ -39,48 +36,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
} }
const docs = await query; const docs = await query;
return sqlNestRelationships({
data: docs, return docs.map((el) => ({
key: "id", ...GatewaysSchema.parse(el),
parentMapper: (data) => ({ orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
...GatewaysSchema.parse(data), identity: { id: el.identityId, name: el.identityName }
identity: { id: data.identityId, name: data.identityName } }));
}),
childrenMapper: [
{
key: "projectId",
label: "projects" as const,
mapper: ({ projectId, projectName, projectSlug }) => ({
id: projectId,
name: projectName,
slug: projectSlug
})
}
]
});
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
} }
}; };
const findByProjectId = async (projectId: string, tx?: Knex) => { return { ...orm, find };
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
)
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
}; };
@@ -4,7 +4,6 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal";
import { import {
TExchangeAllocatedRelayAddressDTO, TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO, TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO, THeartBeatDTO,
TListGatewaysDTO, TListGatewaysDTO,
TUpdateGatewayByIdDTO TUpdateGatewayByIdDTO
} from "./gateway-types"; } from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = { type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory; gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">; orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
@@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({
kmsService, kmsService,
permissionService, permissionService,
orgGatewayConfigDAL, orgGatewayConfigDAL,
keyStore, keyStore
projectGatewayDAL
}: TGatewayServiceFactoryDep) => { }: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
// if (!licenseService.onPremFeatures.gateway) { // if (!licenseService.onPremFeatures.gateway) {
@@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => { const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
orgPermission.type, orgPermission.type,
orgPermission.id, orgPermission.id,
@@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name }); const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
if (projectIds) {
await projectGatewayDAL.transaction(async (tx) => {
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
await projectGatewayDAL.insertMany(
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
tx
);
});
}
return gateway; return gateway;
}; };
@@ -576,27 +562,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => { const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
return gateways;
};
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
const { gatewayId } = projectGateway;
const gateway = await gatewayDAL.findById(gatewayId); const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
@@ -645,8 +611,7 @@ export const gatewayServiceFactory = ({
getGatewayById, getGatewayById,
updateGatewayById, updateGatewayById,
deleteGatewayById, deleteGatewayById,
getProjectGateways, fnGetGatewayClientTlsByGatewayId,
fnGetGatewayClientTls,
heartbeat heartbeat
}; };
}; };
@@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = { export type TUpdateGatewayByIdDTO = {
id: string; id: string;
name?: string; name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor; orgPermission: OrgServiceActor;
}; };
@@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
export const projectGatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectGateway);
return orm;
};
@@ -714,13 +714,15 @@ export const oidcConfigServiceFactory = ({
} }
} }
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({ oidcLogin({
email: claims.email, email: claims.email,
externalId: claims.sub, externalId: claims.sub,
firstName: claims.given_name ?? "", firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "", lastName: claims.family_name ?? "",
orgId: org.id, orgId: org.id,
groups: claims.groups as string[] | undefined, groups,
callbackPort, callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships manageGroupMemberships: oidcCfg.manageGroupMemberships
}) })
@@ -126,7 +126,6 @@ const buildAdminPermissionRules = () => {
can( can(
[ [
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Create,
@@ -207,7 +206,6 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit, ProjectPermissionSecretActions.Edit,
@@ -386,9 +384,10 @@ const buildMemberPermissionRules = () => {
const buildViewerPermissionRules = () => { const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets); can(
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets); [ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue],
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets); ProjectPermissionSub.Secrets
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
@@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
CreateGateways = "create-gateways", CreateGateways = "create-gateways",
ListGateways = "list-gateways", ListGateways = "list-gateways",
EditGateways = "edit-gateways", EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
} }
export enum OrgPermissionIdentityActions { export enum OrgPermissionIdentityActions {
@@ -337,6 +338,7 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -378,6 +380,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
return rules; return rules;
}; };
+52 -1
View File
@@ -16,6 +16,7 @@ export enum ApiDocsTags {
UniversalAuth = "Universal Auth", UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth", GcpAuth = "GCP Auth",
AwsAuth = "AWS Auth", AwsAuth = "AWS Auth",
OciAuth = "OCI Auth",
AzureAuth = "Azure Auth", AzureAuth = "Azure Auth",
KubernetesAuth = "Kubernetes Auth", KubernetesAuth = "Kubernetes Auth",
JwtAuth = "JWT Auth", JwtAuth = "JWT Auth",
@@ -273,6 +274,40 @@ export const AWS_AUTH = {
} }
} as const; } as const;
export const OCI_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
}
} as const;
export const AZURE_AUTH = { export const AZURE_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the identity to login."
@@ -360,6 +395,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience: allowedAudience:
"The optional audience claim that the service account JWT token must have to authenticate with Infisical.", "The optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@@ -376,6 +412,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience: allowedAudience:
"The new optional audience claim that the service account JWT token must have to authenticate with Infisical.", "The new optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.", accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
@@ -573,7 +610,8 @@ export const PROJECTS = {
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project.", autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)", slug: "An optional slug for the project. (must be unique within the organization)",
hasDeleteProtection: "Enable or disable delete protection for the project." hasDeleteProtection: "Enable or disable delete protection for the project.",
secretSharing: "Enable or disable secret sharing for the project."
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
@@ -2102,6 +2140,13 @@ export const AppConnections = {
AZURE_CLIENT_SECRETS: { AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.", code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets." tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
},
OCI: {
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.",
fingerprint: "The fingerprint of the public key uploaded to the user's API keys.",
privateKey: "The private key content in PEM format used to sign API requests."
} }
} }
}; };
@@ -2165,6 +2210,7 @@ export const SecretSyncs = {
const destinationName = SECRET_SYNC_NAME_MAP[destination]; const destinationName = SECRET_SYNC_NAME_MAP[destination];
return { return {
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`,
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.` disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
}; };
}, },
@@ -2249,6 +2295,11 @@ export const SecretSyncs = {
TEAMCITY: { TEAMCITY: {
project: "The TeamCity project to sync secrets to.", project: "The TeamCity project to sync secrets to.",
buildConfig: "The TeamCity build configuration to sync secrets to." buildConfig: "The TeamCity build configuration to sync secrets to."
},
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."
} }
} }
}; };
+46 -14
View File
@@ -174,6 +174,8 @@ const setupProxyServer = async ({
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = net.createServer(); const server = net.createServer();
let streamClosed = false;
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientConn) => { server.on("connection", async (clientConn) => {
try { try {
@@ -202,9 +204,15 @@ const setupProxyServer = async ({
// Handle client connection close // Handle client connection close
clientConn.on("end", () => { clientConn.on("end", () => {
writer.close().catch((err) => { if (!streamClosed) {
logger.error(err); try {
}); writer.close().catch((err) => {
logger.debug(err, "Error closing writer (already closed)");
});
} catch (error) {
logger.debug(error, "Error in writer close");
}
}
}); });
clientConn.on("error", (clientConnErr) => { clientConn.on("error", (clientConnErr) => {
@@ -249,14 +257,29 @@ const setupProxyServer = async ({
setupCopy(); setupCopy();
// Handle connection closure // Handle connection closure
clientConn.on("close", () => { clientConn.on("close", () => {
stream.destroy().catch((err) => { if (!streamClosed) {
proxyErrorMsg.push((err as Error)?.message); streamClosed = true;
}); stream.destroy().catch((err) => {
logger.debug(err, "Stream already destroyed during close event");
});
}
}); });
const cleanup = async () => { const cleanup = async () => {
clientConn?.destroy(); try {
await stream.destroy(); clientConn?.destroy();
} catch (err) {
logger.debug(err, "Error destroying client connection");
}
if (!streamClosed) {
streamClosed = true;
try {
await stream.destroy();
} catch (err) {
logger.debug(err, "Error destroying stream (might be already closed)");
}
}
}; };
clientConn.on("error", (clientConnErr) => { clientConn.on("error", (clientConnErr) => {
@@ -301,8 +324,17 @@ const setupProxyServer = async ({
server, server,
port: address.port, port: address.port,
cleanup: async () => { cleanup: async () => {
server.close(); try {
await quicClient?.destroy(); server.close();
} catch (err) {
logger.debug(err, "Error closing server");
}
try {
await quicClient?.destroy();
} catch (err) {
logger.debug(err, "Error destroying QUIC client");
}
}, },
getProxyError: () => proxyErrorMsg.join(",") getProxyError: () => proxyErrorMsg.join(",")
}); });
@@ -320,10 +352,10 @@ interface ProxyOptions {
orgId: string; orgId: string;
} }
export const withGatewayProxy = async ( export const withGatewayProxy = async <T>(
callback: (port: number) => Promise<void>, callback: (port: number) => Promise<T>,
options: ProxyOptions options: ProxyOptions
): Promise<void> => { ): Promise<T> => {
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server // Setup the proxy server
@@ -339,7 +371,7 @@ export const withGatewayProxy = async (
try { try {
// Execute the callback with the allocated port // Execute the callback with the allocated port
await callback(port); return await callback(port);
} catch (err) { } catch (err) {
const proxyErrorMessage = getProxyError(); const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) { if (proxyErrorMessage) {
+2 -2
View File
@@ -32,13 +32,13 @@ export const buildFindFilter =
<R extends object = object>( <R extends object = object>(
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>, { $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
tableName?: TableName, tableName?: TableName,
excludeKeys?: Array<keyof R> excludeKeys?: string[]
) => ) =>
(bd: Knex.QueryBuilder<R, R>) => { (bd: Knex.QueryBuilder<R, R>) => {
const processedFilter = tableName const processedFilter = tableName
? Object.fromEntries( ? Object.fromEntries(
Object.entries(filter) Object.entries(filter)
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R)) .filter(([key]) => !excludeKeys || !excludeKeys.includes(key))
.map(([key, value]) => [`${tableName}.${key}`, value]) .map(([key, value]) => [`${tableName}.${key}`, value])
) )
: filter; : filter;
+3 -1
View File
@@ -57,7 +57,9 @@ export const registerServeUI = async (
reply.callNotFound(); reply.callNotFound();
return; return;
} }
return reply.sendFile("index.html"); // reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers
// to avoid ui bundle skew on new deployment
return reply.sendFile("index.html", { maxAge: 0, immutable: false });
} }
}); });
} }
+25 -13
View File
@@ -32,7 +32,6 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal";
@@ -165,6 +164,8 @@ import { identityKubernetesAuthDALFactory } from "@app/services/identity-kuberne
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service"; import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal"; import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service"; import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { identityOciAuthDALFactory } from "@app/services/identity-oci-auth/identity-oci-auth-dal";
import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal"; import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service"; import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@@ -358,6 +359,7 @@ export const registerRoutes = async (
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db); const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db); const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOciAuthDAL = identityOciAuthDALFactory(db);
const identityOidcAuthDAL = identityOidcAuthDALFactory(db); const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityJwtAuthDAL = identityJwtAuthDALFactory(db); const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db); const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
@@ -439,7 +441,6 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db); const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db); const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
@@ -1374,12 +1375,24 @@ export const registerRoutes = async (
identityUaDAL, identityUaDAL,
licenseService licenseService
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
licenseService, licenseService,
gatewayService,
gatewayDAL,
kmsService kmsService
}); });
const identityGcpAuthService = identityGcpAuthServiceFactory({ const identityGcpAuthService = identityGcpAuthServiceFactory({
@@ -1406,6 +1419,14 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const identityOciAuthService = identityOciAuthServiceFactory({
identityAccessTokenDAL,
identityOciAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
});
const identityOidcAuthService = identityOidcAuthServiceFactory({ const identityOidcAuthService = identityOidcAuthServiceFactory({
identityOidcAuthDAL, identityOidcAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
@@ -1434,16 +1455,6 @@ export const registerRoutes = async (
identityDAL identityDAL
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({ const dynamicSecretProviders = buildDynamicSecretProviders({
gatewayService gatewayService
}); });
@@ -1465,7 +1476,7 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}); });
@@ -1768,6 +1779,7 @@ export const registerRoutes = async (
identityGcpAuth: identityGcpAuthService, identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService, identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService, identityAzureAuth: identityAzureAuthService,
identityOciAuth: identityOciAuthService,
identityOidcAuth: identityOidcAuthService, identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService, identityJwtAuth: identityJwtAuthService,
identityLdapAuth: identityLdapAuthService, identityLdapAuth: identityLdapAuthService,
@@ -263,7 +263,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
pitVersionLimit: true, pitVersionLimit: true,
kmsCertificateKeyId: true, kmsCertificateKeyId: true,
auditLogsRetentionDays: true, auditLogsRetentionDays: true,
hasDeleteProtection: true hasDeleteProtection: true,
secretSharing: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({ export const SanitizedTagSchema = SecretTagsSchema.pick({
@@ -38,6 +38,7 @@ import {
} from "@app/services/app-connection/humanitec"; } from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci";
import { import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
@@ -76,7 +77,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureClientSecretsConnectionSchema.options, ...SanitizedAzureClientSecretsConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options,
...SanitizedLdapConnectionSchema.options, ...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options ...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -97,7 +99,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureClientSecretsConnectionListItemSchema, AzureClientSecretsConnectionListItemSchema,
WindmillConnectionListItemSchema, WindmillConnectionListItemSchema,
LdapConnectionListItemSchema, LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -13,6 +13,7 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerOCIConnectionRouter } from "./oci-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
@@ -40,5 +41,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Auth0]: registerAuth0ConnectionRouter, [AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.HCVault]: registerHCVaultConnectionRouter, [AppConnection.HCVault]: registerHCVaultConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter, [AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter [AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter
}; };
@@ -0,0 +1,123 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateOCIConnectionSchema,
SanitizedOCIConnectionSchema,
UpdateOCIConnectionSchema
} from "@app/services/app-connection/oci";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./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;
}
});
};
@@ -132,7 +132,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate), certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain), certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey), privateKey: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes) serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
}) })
} }
@@ -3,6 +3,7 @@ import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -21,7 +22,8 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
kubernetesHost: true, kubernetesHost: true,
allowedNamespaces: true, allowedNamespaces: true,
allowedNames: true, allowedNames: true,
allowedAudience: true allowedAudience: true,
gatewayId: true
}).extend({ }).extend({
caCert: z.string(), caCert: z.string(),
tokenReviewerJwt: z.string().optional().nullable() tokenReviewerJwt: z.string().optional().nullable()
@@ -100,12 +102,30 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}), }),
body: z body: z
.object({ .object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), kubernetesHost: z
.string()
.trim()
.min(1)
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
.refine(
(val) =>
characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash
])(val),
{
message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes."
}
),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.ATTACH.gatewayId),
accessTokenTrustedIps: z accessTokenTrustedIps: z
.object({ .object({
ipAddress: z.string().trim() ipAddress: z.string().trim()
@@ -199,12 +219,34 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}), }),
body: z body: z
.object({ .object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), kubernetesHost: z
.string()
.trim()
.min(1)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
.refine(
(val) => {
if (!val) return true;
return characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash
])(val);
},
{
message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes."
}
),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.UPDATE.gatewayId),
accessTokenTrustedIps: z accessTokenTrustedIps: z
.object({ .object({
ipAddress: z.string().trim() ipAddress: z.string().trim()
@@ -0,0 +1,338 @@
import { z } from "zod";
import { IdentityOciAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
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 { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateTenancy, validateUsernames } from "@app/services/identity-oci-auth/identity-oci-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/oci-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Login with OCI Auth",
body: z.object({
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
headers: z
.object({
authorization: z.string(),
host: z.string(),
"x-date": z.string()
})
.describe(OCI_AUTH.LOGIN.headers)
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityOciAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityOciAuthId: identityOciAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Attach OCI Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(OCI_AUTH.ATTACH.identityId)
}),
body: z
.object({
tenancyOcid: validateTenancy.describe(OCI_AUTH.ATTACH.tenancyOcid),
allowedUsernames: validateUsernames.describe(OCI_AUTH.ATTACH.allowedUsernames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(OCI_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(OCI_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(OCI_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OCI_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.attachOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
tenancyOcid: identityOciAuth.tenancyOcid,
allowedUsernames: identityOciAuth.allowedUsernames || null,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "PATCH",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Update OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.UPDATE.identityId)
}),
body: z
.object({
tenancyOcid: validateTenancy.describe(OCI_AUTH.UPDATE.tenancyOcid),
allowedUsernames: validateUsernames.describe(OCI_AUTH.UPDATE.allowedUsernames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(OCI_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(OCI_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(OCI_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.min(0)
.optional()
.describe(OCI_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.updateOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
allowedUsernames: req.body.allowedUsernames || null
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
tenancyOcid: identityOciAuth.tenancyOcid,
allowedUsernames: identityOciAuth.allowedUsernames || null,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "GET",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Retrieve OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.RETRIEVE.identityId)
}),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.getOciAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.GET_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "DELETE",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Delete OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.revokeIdentityOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId
}
}
});
return { identityOciAuth };
}
});
};
@@ -52,7 +52,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
identity: IdentitiesSchema.extend({ identity: IdentitiesSchema.extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string()),
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
}) })
}) })
} }
@@ -123,7 +124,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
identity: IdentitiesSchema identity: IdentitiesSchema.extend({
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
})
}) })
} }
}, },
@@ -227,8 +230,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
identity: IdentityOrgMembershipsSchema.extend({ identity: IdentityOrgMembershipsSchema.extend({
metadata: z metadata: z
.object({ .object({
key: z.string().trim().min(1),
id: z.string().trim().min(1), id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1) value: z.string().trim().min(1)
}) })
.array() .array()
+2
View File
@@ -21,6 +21,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router"; import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router"; import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router"; import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -64,6 +65,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAccessTokenRouter); await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter); await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter); await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOciAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter); await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter); await authRouter.register(registerIdentityJwtAuthRouter);
await authRouter.register(registerIdentityLdapAuthRouter); await authRouter.register(registerIdentityLdapAuthRouter);
@@ -275,7 +275,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}, },
{ message: "Duration value must be at least 1" } { message: "Duration value must be at least 1" }
) )
.optional() .optional(),
secretsProductEnabled: z.boolean().optional(),
pkiProductEnabled: z.boolean().optional(),
kmsProductEnabled: z.boolean().optional(),
sshProductEnabled: z.boolean().optional(),
scannerProductEnabled: z.boolean().optional(),
shareSecretsProductEnabled: z.boolean().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -346,7 +346,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore." "Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
}) })
.optional() .optional()
.describe(PROJECTS.UPDATE.slug) .describe(PROJECTS.UPDATE.slug),
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -366,7 +367,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization, autoCapitalization: req.body.autoCapitalization,
hasDeleteProtection: req.body.hasDeleteProtection, hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug slug: req.body.slug,
secretSharing: req.body.secretSharing
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -511,7 +513,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const workspace = await server.services.project.updateAuditLogsRetention({ const workspace = await server.services.project.updateAuditLogsRetention({
actorId: req.permission.id, actorId: req.permission.id,
@@ -10,6 +10,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-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.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter, [SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.HCVault]: registerHCVaultSyncRouter, [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 { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; 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 { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -43,7 +44,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
VercelSyncSchema, VercelSyncSchema,
WindmillSyncSchema, WindmillSyncSchema,
HCVaultSyncSchema, HCVaultSyncSchema,
TeamCitySyncSchema TeamCitySyncSchema,
OCIVaultSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -60,7 +62,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
VercelSyncListItemSchema, VercelSyncListItemSchema,
WindmillSyncListItemSchema, WindmillSyncListItemSchema,
HCVaultSyncListItemSchema, HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -16,7 +16,8 @@ export enum AppConnection {
Auth0 = "auth0", Auth0 = "auth0",
HCVault = "hashicorp-vault", HCVault = "hashicorp-vault",
LDAP = "ldap", LDAP = "ldap",
TeamCity = "teamcity" TeamCity = "teamcity",
OCI = "oci"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -53,6 +53,7 @@ import {
} from "./humanitec"; } from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { import {
getTeamCityConnectionListItem, getTeamCityConnectionListItem,
@@ -91,7 +92,8 @@ export const listAppConnectionOptions = () => {
getAuth0ConnectionListItem(), getAuth0ConnectionListItem(),
getHCVaultConnectionListItem(), getHCVaultConnectionListItem(),
getLdapConnectionListItem(), getLdapConnectionListItem(),
getTeamCityConnectionListItem() getTeamCityConnectionListItem(),
getOCIConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -160,7 +162,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -176,6 +179,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
return "OAuth"; return "OAuth";
case AwsConnectionMethod.AccessKey: case AwsConnectionMethod.AccessKey:
case OCIConnectionMethod.AccessKey:
return "Access Key"; return "Access Key";
case AwsConnectionMethod.AssumeRole: case AwsConnectionMethod.AssumeRole:
return "Assume Role"; return "Assume Role";
@@ -250,5 +254,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Auth0]: platformManagedCredentialsNotSupported, [AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.HCVault]: platformManagedCredentialsNotSupported, [AppConnection.HCVault]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported [AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported
}; };
@@ -18,5 +18,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Auth0]: "Auth0", [AppConnection.Auth0]: "Auth0",
[AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP", [AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity" [AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI"
}; };
@@ -49,6 +49,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateOCIConnectionCredentialsSchema } from "./oci";
import { ociConnectionService } from "./oci/oci-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
@@ -85,7 +87,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema, [AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema, [AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema, [AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema [AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -464,6 +467,7 @@ export const appConnectionServiceFactory = ({
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
hcvault: hcVaultConnectionService(connectAppConnectionById), hcvault: hcVaultConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById) teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById)
}; };
}; };
@@ -76,6 +76,12 @@ import {
TValidateLdapConnectionCredentialsSchema TValidateLdapConnectionCredentialsSchema
} from "./ldap"; } from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TOCIConnection,
TOCIConnectionConfig,
TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema
} from "./oci";
import { import {
TPostgresConnection, TPostgresConnection,
TPostgresConnectionInput, TPostgresConnectionInput,
@@ -125,6 +131,7 @@ export type TAppConnection = { id: string } & (
| THCVaultConnection | THCVaultConnection
| TLdapConnection | TLdapConnection
| TTeamCityConnection | TTeamCityConnection
| TOCIConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -150,6 +157,7 @@ export type TAppConnectionInput = { id: string } & (
| THCVaultConnectionInput | THCVaultConnectionInput
| TLdapConnectionInput | TLdapConnectionInput
| TTeamCityConnectionInput | TTeamCityConnectionInput
| TOCIConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -180,7 +188,8 @@ export type TAppConnectionConfig =
| TAuth0ConnectionConfig | TAuth0ConnectionConfig
| THCVaultConnectionConfig | THCVaultConnectionConfig
| TLdapConnectionConfig | TLdapConnectionConfig
| TTeamCityConnectionConfig; | TTeamCityConnectionConfig
| TOCIConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -200,7 +209,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateAuth0ConnectionCredentialsSchema | TValidateAuth0ConnectionCredentialsSchema
| TValidateHCVaultConnectionCredentialsSchema | TValidateHCVaultConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema | TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema; | TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -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,70 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
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;
};
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) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
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
) => {
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 "../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;
};
@@ -593,18 +593,27 @@ export const certificateServiceFactory = ({
certificateChain = `${caCert}\n${caCertChain}`.trim(); certificateChain = `${caCert}\n${caCertChain}`.trim();
} }
const { certPrivateKey } = await getCertificateCredentials({ let privateKey: string | null = null;
certId: cert.id, try {
projectId: cert.projectId, const { certPrivateKey } = await getCertificateCredentials({
certificateSecretDAL, certId: cert.id,
projectDAL, projectId: cert.projectId,
kmsService certificateSecretDAL,
}); projectDAL,
kmsService
});
privateKey = certPrivateKey;
} catch (e) {
// Skip NotFound errors but throw all others
if (!(e instanceof NotFoundError)) {
throw e;
}
}
return { return {
certificate, certificate,
certificateChain, certificateChain,
privateKey: certPrivateKey, privateKey,
serialNumber, serialNumber,
cert cert
}; };
@@ -36,6 +36,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`)
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
@@ -46,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
@@ -63,6 +65,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws, trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOciAuth: doc.accessTokenTrustedIpsOci,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt, trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
@@ -182,6 +182,7 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth, [IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth, [IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth, [IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
@@ -4,8 +4,14 @@ import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
OrgPermissionGatewayActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@@ -13,6 +19,7 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -43,6 +50,8 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: TGatewayServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
}; };
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>; export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
@@ -53,8 +62,45 @@ export const identityKubernetesAuthServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
licenseService, licenseService,
gatewayService,
gatewayDAL,
kmsService kmsService
}: TIdentityKubernetesAuthServiceFactoryDep) => { }: TIdentityKubernetesAuthServiceFactoryDep) => {
const $gatewayProxyWrapper = async <T>(
inputs: {
gatewayId: string;
targetHost: string;
targetPort: number;
},
gatewayCallback: (host: string, port: number) => Promise<T>
): Promise<T> => {
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
const callbackResult = await withGatewayProxy(
async (port) => {
// Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
const res = await gatewayCallback("https://localhost", port);
return res;
},
{
targetHost: inputs.targetHost,
targetPort: inputs.targetPort,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
return callbackResult;
};
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) { if (!identityKubernetesAuth) {
@@ -92,46 +138,65 @@ export const identityKubernetesAuthServiceFactory = ({
tokenReviewerJwt = serviceAccountJwt; tokenReviewerJwt = serviceAccountJwt;
} }
const { data } = await axios const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
.post<TCreateTokenReviewResponse>( const baseUrl = port ? `${host}:${port}` : host;
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) { const res = await axios
throw new UnauthorizedError({ .post<TCreateTokenReviewResponse>(
message, `${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
name: "KubernetesTokenReviewRequestError" {
}); apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
} }
} }
} throw err;
throw err; });
});
return res.data;
};
const [k8sHost, k8sPort] = identityKubernetesAuth.kubernetesHost.split(":");
const data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443
},
tokenReviewCallback
)
: await tokenReviewCallback();
if ("error" in data.status) if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
@@ -222,6 +287,7 @@ export const identityKubernetesAuthServiceFactory = ({
const attachKubernetesAuth = async ({ const attachKubernetesAuth = async ({
identityId, identityId,
gatewayId,
kubernetesHost, kubernetesHost,
caCert, caCert,
tokenReviewerJwt, tokenReviewerJwt,
@@ -280,6 +346,27 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
@@ -296,6 +383,7 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
gatewayId,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt
? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob
@@ -318,6 +406,7 @@ export const identityKubernetesAuthServiceFactory = ({
allowedNamespaces, allowedNamespaces,
allowedNames, allowedNames,
allowedAudience, allowedAudience,
gatewayId,
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
@@ -373,11 +462,33 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const updateQuery: TIdentityKubernetesAuthsUpdate = { const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost, kubernetesHost,
allowedNamespaces, allowedNamespaces,
allowedNames, allowedNames,
allowedAudience, allowedAudience,
gatewayId,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
@@ -13,6 +13,7 @@ export type TAttachKubernetesAuthDTO = {
allowedNamespaces: string; allowedNamespaces: string;
allowedNames: string; allowedNames: string;
allowedAudience: string; allowedAudience: string;
gatewayId?: string | null;
accessTokenTTL: number; accessTokenTTL: number;
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number; accessTokenNumUsesLimit: number;
@@ -28,6 +29,7 @@ export type TUpdateKubernetesAuthDTO = {
allowedNamespaces?: string; allowedNamespaces?: string;
allowedNames?: string; allowedNames?: string;
allowedAudience?: string; allowedAudience?: string;
gatewayId?: string | null;
accessTokenTTL?: number; accessTokenTTL?: number;
accessTokenMaxTTL?: number; accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number; accessTokenNumUsesLimit?: number;
@@ -0,0 +1,9 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityOciAuthDALFactory = ReturnType<typeof identityOciAuthDALFactory>;
export const identityOciAuthDALFactory = (db: TDbClient) => {
return ormify(db, TableName.IdentityOciAuth);
};
@@ -0,0 +1,368 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { AxiosError } from "axios";
import jwt from "jsonwebtoken";
import RE2 from "re2";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal";
import {
TAttachOciAuthDTO,
TGetOciAuthDTO,
TLoginOciAuthDTO,
TOciGetUserResponse,
TRevokeOciAuthDTO,
TUpdateOciAuthDTO
} from "./identity-oci-auth-types";
type TIdentityOciAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityOciAuthDAL: Pick<TIdentityOciAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export type TIdentityOciAuthServiceFactory = ReturnType<typeof identityOciAuthServiceFactory>;
export const identityOciAuthServiceFactory = ({
identityAccessTokenDAL,
identityOciAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
}: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) {
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId });
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
});
}
const { data } = await request
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
headers
})
.catch((err: AxiosError) => {
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
throw err;
});
if (data.compartmentId !== identityOciAuth.tenancyOcid) {
throw new UnauthorizedError({
message: "Access denied: OCI account isn't part of tenancy."
});
}
if (identityOciAuth.allowedUsernames) {
const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: OCI account username not allowed."
});
}
// Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOciAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OCI_AUTH
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return {
identityOciAuth,
accessToken,
identityAccessToken,
identityMembershipOrg
};
};
const attachOciAuth = async ({
identityId,
tenancyOcid,
allowedUsernames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId,
isActorSuperAdmin
}: TAttachOciAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "Failed to add OCI Auth to already configured identity"
});
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
const doc = await identityOciAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
type: "iam",
tenancyOcid,
allowedUsernames,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
return doc;
});
return { ...identityOciAuth, orgId: identityMembershipOrg.orgId };
};
const updateOciAuth = async ({
identityId,
tenancyOcid,
allowedUsernames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new NotFoundError({
message: "The identity does not have OCI Auth attached"
});
}
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityOciAuth.accessTokenTTL) > (accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedOciAuth = await identityOciAuthDAL.updateById(identityOciAuth.id, {
tenancyOcid,
allowedUsernames,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId };
};
const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "The identity does not have OCI Auth attached"
});
}
const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityOciAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "The identity does not have OCI auth"
});
}
const { permission, membership } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke OCI auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx);
return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityOciAuth;
};
return {
login,
attachOciAuth,
updateOciAuth,
getOciAuth,
revokeIdentityOciAuth
};
};
@@ -0,0 +1,53 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginOciAuthDTO = {
identityId: string;
userOcid: string;
headers: {
authorization: string;
host: string;
"x-date": string;
};
};
export type TAttachOciAuthDTO = {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOciAuthDTO = {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetOciAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeOciAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TOciGetUserResponse = {
email: string;
emailVerified: boolean;
timeModified: string;
isMfaActivated: boolean;
id: string;
compartmentId: string;
name: string;
timeCreated: string;
freeformTags: { [key: string]: string };
lifecycleState: string;
};
@@ -0,0 +1,32 @@
import RE2 from "re2";
import { z } from "zod";
const usernameSchema = z
.string()
.min(1, "Username cannot be empty")
.refine((val) => new RE2("^[a-zA-Z0-9._@-]+$").test(val), "Invalid OCI username format");
export const validateUsernames = z
.string()
.trim()
.max(500, "Input exceeds the maximum limit of 500 characters")
.nullish()
.transform((val) => {
if (!val) return [];
return val
.split(",")
.map((s) => s.trim())
.filter(Boolean);
})
.refine((arr) => arr.every((name) => usernameSchema.safeParse(name).success), {
message: "One or more usernames are invalid"
})
.transform((arr) => (arr.length > 0 ? arr.join(", ") : null));
export const validateTenancy = z
.string()
.trim()
.min(1, "Tenancy OCID cannot be empty.")
.refine(
(val) => new RE2("^ocid1\\.tenancy\\.oc1\\..+$").test(val),
"Invalid Tenancy OCID format. Must start with ocid1.tenancy.oc1."
);
@@ -8,6 +8,7 @@ import {
TIdentityAzureAuths, TIdentityAzureAuths,
TIdentityGcpAuths, TIdentityGcpAuths,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityTokenAuths, TIdentityTokenAuths,
TIdentityUniversalAuths TIdentityUniversalAuths
@@ -66,6 +67,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin(
TableName.IdentityOciAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin( .leftJoin(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.IdentityProjectMembership}.identityId`, `${TableName.IdentityProjectMembership}.identityId`,
@@ -107,6 +113,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
@@ -270,6 +277,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>( .leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.Identity}.id`, `${TableName.Identity}.id`,
@@ -309,6 +321,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth) db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
@@ -336,6 +349,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -356,6 +370,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId tokenId
@@ -5,6 +5,7 @@ export const buildAuthMethods = ({
gcpId, gcpId,
awsId, awsId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -15,6 +16,7 @@ export const buildAuthMethods = ({
gcpId?: string; gcpId?: string;
awsId?: string; awsId?: string;
kubernetesId?: string; kubernetesId?: string;
ociId?: string;
oidcId?: string; oidcId?: string;
azureId?: string; azureId?: string;
tokenId?: string; tokenId?: string;
@@ -26,6 +28,7 @@ export const buildAuthMethods = ({
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null], ...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
...[awsId ? IdentityAuthMethod.AWS_AUTH : null], ...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null], ...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[ociId ? IdentityAuthMethod.OCI_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null], ...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null], ...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null], ...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
@@ -8,6 +8,7 @@ import {
TIdentityGcpAuths, TIdentityGcpAuths,
TIdentityJwtAuths, TIdentityJwtAuths,
TIdentityKubernetesAuths, TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths, TIdentityOidcAuths,
TIdentityOrgMemberships, TIdentityOrgMemberships,
TIdentityTokenAuths, TIdentityTokenAuths,
@@ -62,6 +63,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>( .leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
@@ -95,6 +101,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -186,6 +193,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>( .leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
"paginatedIdentity.identityId", "paginatedIdentity.identityId",
@@ -226,6 +238,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -269,6 +282,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
gcpId, gcpId,
jwtId, jwtId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -301,6 +315,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -401,6 +416,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId` `${TableName.IdentityKubernetesAuth}.identityId`
) )
.leftJoin(
TableName.IdentityOciAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin( .leftJoin(
TableName.IdentityOidcAuth, TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityOrgMembership}.identityId`,
@@ -441,6 +461,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -485,6 +506,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
gcpId, gcpId,
jwtId, jwtId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -517,6 +539,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
awsId, awsId,
gcpId, gcpId,
kubernetesId, kubernetesId,
ociId,
oidcId, oidcId,
azureId, azureId,
tokenId, tokenId,
@@ -106,18 +106,29 @@ export const identityServiceFactory = ({
}, },
tx tx
); );
let insertedMetadata: Array<{
id: string;
key: string;
value: string;
}> = [];
if (metadata && metadata.length) { if (metadata && metadata.length) {
await identityMetadataDAL.insertMany( const rowsToInsert = metadata.map(({ key, value }) => ({
metadata.map(({ key, value }) => ({ identityId: newIdentity.id,
identityId: newIdentity.id, orgId,
orgId, key,
key, value
value }));
})),
tx insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
);
} }
return { ...newIdentity, authMethods: [] };
return {
...newIdentity,
authMethods: [],
metadata: insertedMetadata
};
}); });
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
@@ -189,21 +200,31 @@ export const identityServiceFactory = ({
tx tx
); );
} }
let insertedMetadata: Array<{
id: string;
key: string;
value: string;
}> = [];
if (metadata) { if (metadata) {
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx); await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
if (metadata.length) { if (metadata.length) {
await identityMetadataDAL.insertMany( const rowsToInsert = metadata.map(({ key, value }) => ({
metadata.map(({ key, value }) => ({ identityId: newIdentity.id,
identityId: newIdentity.id, orgId: identityOrgMembership.orgId,
orgId: identityOrgMembership.orgId, key,
key, value
value }));
})),
tx insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
);
} }
} }
return newIdentity;
return {
...newIdentity,
metadata: insertedMetadata
};
}); });
return { ...identity, orgId: identityOrgMembership.orgId }; return { ...identity, orgId: identityOrgMembership.orgId };
@@ -224,6 +245,7 @@ export const identityServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return identity; return identity;
}; };
+7 -1
View File
@@ -18,5 +18,11 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
privilegeUpgradeInitiatedByUsername: true, privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true, privilegeUpgradeInitiatedAt: true,
bypassOrgAuthEnabled: true, bypassOrgAuthEnabled: true,
userTokenExpiration: true userTokenExpiration: true,
secretsProductEnabled: true,
pkiProductEnabled: true,
kmsProductEnabled: true,
sshProductEnabled: true,
scannerProductEnabled: true,
shareSecretsProductEnabled: true
}); });
+14 -2
View File
@@ -355,7 +355,13 @@ export const orgServiceFactory = ({
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization, allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled, bypassOrgAuthEnabled,
userTokenExpiration userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled
} }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -457,7 +463,13 @@ export const orgServiceFactory = ({
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization, allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled, bypassOrgAuthEnabled,
userTokenExpiration userTokenExpiration,
secretsProductEnabled,
pkiProductEnabled,
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
shareSecretsProductEnabled
}); });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org; return org;
+6
View File
@@ -75,6 +75,12 @@ export type TUpdateOrgDTO = {
allowSecretSharingOutsideOrganization: boolean; allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean; bypassOrgAuthEnabled: boolean;
userTokenExpiration: string; userTokenExpiration: string;
secretsProductEnabled: boolean;
pkiProductEnabled: boolean;
kmsProductEnabled: boolean;
sshProductEnabled: boolean;
scannerProductEnabled: boolean;
shareSecretsProductEnabled: boolean;
}>; }>;
} & TOrgPermission; } & TOrgPermission;
@@ -660,7 +660,8 @@ export const projectServiceFactory = ({
autoCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization, enforceCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection, hasDeleteProtection: update.hasDeleteProtection,
slug: update.slug slug: update.slug,
secretSharing: update.secretSharing
}); });
return updatedProject; return updatedProject;
@@ -93,6 +93,7 @@ export type TUpdateProjectDTO = {
autoCapitalization?: boolean; autoCapitalization?: boolean;
hasDeleteProtection?: boolean; hasDeleteProtection?: boolean;
slug?: string; slug?: string;
secretSharing?: boolean;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -2,6 +2,7 @@ import AWS, { AWSError } from "aws-sdk";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; 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"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types"; import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
@@ -389,6 +390,9 @@ export const AwsParameterStoreSyncFns = {
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, parameter] = entry; const [key, parameter] = entry;
// eslint-disable-next-line no-continue
if (!matchesSchema(key, syncOptions.keySchema)) continue;
if (!(key in secretMap) || !secretMap[key].value) { if (!(key in secretMap) || !secretMap[key].value) {
parametersToDelete.push(parameter); parametersToDelete.push(parameter);
} }
@@ -27,6 +27,7 @@ import {
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; 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"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types"; import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types";
@@ -399,6 +400,9 @@ export const AwsSecretsManagerSyncFns = {
if (syncOptions.disableSecretDeletion) return; if (syncOptions.disableSecretDeletion) return;
for await (const secretKey of Object.keys(awsSecretsRecord)) { for await (const secretKey of Object.keys(awsSecretsRecord)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secretKey, syncOptions.keySchema)) continue;
if (!(secretKey in secretMap) || !secretMap[secretKey].value) { if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
try { try {
await deleteSecret(client, secretKey); await deleteSecret(client, secretKey);
@@ -7,6 +7,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns"; import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types"; import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
@@ -139,6 +140,9 @@ export const azureAppConfigurationSyncFactory = ({
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const key of Object.keys(azureAppConfigSecrets)) { for await (const key of Object.keys(azureAppConfigSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
const azureSecret = azureAppConfigSecrets[key]; const azureSecret = azureAppConfigSecrets[key];
if ( if (
!(key in secretMap) || !(key in secretMap) ||
@@ -5,6 +5,7 @@ import { request } from "@app/lib/config/request";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SecretSyncError } from "../secret-sync-errors"; import { SecretSyncError } from "../secret-sync-errors";
@@ -192,7 +193,9 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const deleteSecretKey of deleteSecrets.filter( for await (const deleteSecretKey of deleteSecrets.filter(
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret) (secret) =>
matchesSchema(secret, secretSync.syncOptions.keySchema) &&
!setSecrets.find((setSecret) => setSecret.key === secret)
)) { )) {
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, {
headers: { headers: {
@@ -12,6 +12,7 @@ import {
TCamundaSyncWithCredentials TCamundaSyncWithCredentials
} from "@app/services/secret-sync/camunda/camunda-sync-types"; } from "@app/services/secret-sync/camunda/camunda-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "../secret-sync-types"; import { TSecretMap } from "../secret-sync-types";
@@ -116,6 +117,9 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of Object.keys(camundaSecrets)) { for await (const secret of Object.keys(camundaSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue;
if (!(secret in secretMap) || !secretMap[secret].value) { if (!(secret in secretMap) || !secretMap[secret].value) {
try { try {
await deleteCamundaSecret({ await deleteCamundaSecret({
@@ -11,6 +11,7 @@ import {
TDatabricksSyncWithCredentials TDatabricksSyncWithCredentials
} from "@app/services/secret-sync/databricks/databricks-sync-types"; } from "@app/services/secret-sync/databricks/databricks-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "../secret-sync-types"; import { TSecretMap } from "../secret-sync-types";
@@ -115,6 +116,9 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of databricksSecretKeys) { for await (const secret of databricksSecretKeys) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue;
if (!(secret.key in secretMap)) { if (!(secret.key in secretMap)) {
await deleteDatabricksSecrets({ await deleteDatabricksSecrets({
key: secret.key, key: secret.key,
@@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp"; import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors"; import { SecretSyncError } from "../secret-sync-errors";
import { TSecretMap } from "../secret-sync-types"; import { TSecretMap } from "../secret-sync-types";
@@ -153,6 +154,9 @@ export const GcpSyncFns = {
} }
for await (const key of Object.keys(gcpSecrets)) { for await (const key of Object.keys(gcpSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
try { try {
if (!(key in secretMap) || !secretMap[key].value) { if (!(key in secretMap) || !secretMap[key].value) {
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
@@ -4,6 +4,7 @@ import sodium from "libsodium-wrappers";
import { getGitHubClient } from "@app/services/app-connection/github"; import { getGitHubClient } from "@app/services/app-connection/github";
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -222,6 +223,9 @@ export const GithubSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const encryptedSecret of encryptedSecrets) { for await (const encryptedSecret of encryptedSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue;
if (!(encryptedSecret.name in secretMap)) { if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret); await deleteSecret(client, secretSync, encryptedSecret);
} }
@@ -11,6 +11,7 @@ import {
TPostHCVaultVariable TPostHCVaultVariable
} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types"; } from "@app/services/secret-sync/hc-vault/hc-vault-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; 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"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => {
@@ -68,7 +69,7 @@ export const HCVaultSyncFns = {
const { const {
connection, connection,
destinationConfig: { mount, path }, destinationConfig: { mount, path },
syncOptions: { disableSecretDeletion } syncOptions: { disableSecretDeletion, keySchema }
} = secretSync; } = secretSync;
const { namespace } = connection.credentials; const { namespace } = connection.credentials;
@@ -95,6 +96,9 @@ export const HCVaultSyncFns = {
if (disableSecretDeletion) return; if (disableSecretDeletion) return;
for await (const [key] of Object.entries(variables)) { for await (const [key] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!(key in secretMap)) { if (!(key in secretMap)) {
delete variables[key]; delete variables[key];
tainted = true; tainted = true;
@@ -2,6 +2,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -199,6 +200,9 @@ export const HumanitecSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const humanitecSecret of humanitecSecrets) { for await (const humanitecSecret of humanitecSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!secretMap[humanitecSecret.key]) { if (!secretMap[humanitecSecret.key]) {
await deleteSecret(secretSync, humanitecSecret); await deleteSecret(secretSync, humanitecSecret);
} }
@@ -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,296 @@
import { secrets, vault } from "oci-sdk";
import { delay } from "@app/lib/delay";
import { getOCIProvider } from "@app/services/app-connection/oci";
import {
TCreateOCIVaultVariable,
TDeleteOCIVaultVariable,
TOCIVaultListVariables,
TOCIVaultSyncWithCredentials,
TUnmarkOCIVaultVariableFromDeletion,
TUpdateOCIVaultVariable
} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types";
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,70 @@
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)
});
@@ -0,0 +1,48 @@
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;
};
export type TUnmarkOCIVaultVariableFromDeletion = TOCIVaultListVariables & {
secretId: string;
};
@@ -12,7 +12,8 @@ export enum SecretSync {
Vercel = "vercel", Vercel = "vercel",
Windmill = "windmill", Windmill = "windmill",
HCVault = "hashicorp-vault", HCVault = "hashicorp-vault",
TeamCity = "teamcity" TeamCity = "teamcity",
OCIVault = "oci-vault"
} }
export enum SecretSyncInitialSyncBehavior { export enum SecretSyncInitialSyncBehavior {
@@ -1,4 +1,5 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import RE2 from "re2";
import { import {
AWS_PARAMETER_STORE_SYNC_LIST_OPTION, AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -28,6 +29,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; 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 { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
@@ -47,7 +49,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
[SecretSync.HCVault]: HC_VAULT_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 = () => { export const listSecretSyncOptions = () => {
@@ -59,45 +62,63 @@ type TSyncSecretDeps = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { // Add schema to secret keys
// let secretMap = { ...unprocessedSecretMap }; const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
// if (!schema) return unprocessedSecretMap;
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
// const processedSecretMap: TSecretMap = {};
// if (appendSuffix || prependPrefix) {
// secretMap = {}; for (const [key, value] of Object.entries(unprocessedSecretMap)) {
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { const newKey = new RE2("{{secretKey}}").replace(schema, key);
// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value; processedSecretMap[newKey] = value;
// }); }
// }
// return processedSecretMap;
// return secretMap; };
// };
// // Strip schema from secret keys
// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
// let secretMap = { ...unprocessedSecretMap }; if (!schema) return unprocessedSecretMap;
//
// const { appendSuffix, prependPrefix } = secretSync.syncOptions; const [prefix, suffix] = schema.split("{{secretKey}}");
//
// if (appendSuffix || prependPrefix) { const strippedMap: TSecretMap = {};
// secretMap = {};
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { for (const [key, value] of Object.entries(unprocessedSecretMap)) {
// let processedKey = key; if (!key.startsWith(prefix) || !key.endsWith(suffix)) {
// // eslint-disable-next-line no-continue
// if (prependPrefix && processedKey.startsWith(prependPrefix)) { continue;
// processedKey = processedKey.slice(prependPrefix.length); }
// }
// const strippedKey = key.slice(prefix.length, key.length - suffix.length);
// if (appendSuffix && processedKey.endsWith(appendSuffix)) { strippedMap[strippedKey] = value;
// processedKey = processedKey.slice(0, -appendSuffix.length); }
// }
// return strippedMap;
// secretMap[processedKey] = value; };
// });
// } // Checks if a key matches a schema
// export const matchesSchema = (key: string, schema?: string): boolean => {
// return secretMap; if (!schema) return true;
// };
const [prefix, suffix] = schema.split("{{secretKey}}");
if (prefix === undefined || suffix === undefined) return true;
return key.startsWith(prefix) && key.endsWith(suffix);
};
// Filter only for secrets with keys that match the schema
const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => {
const filteredMap: TSecretMap = {};
for (const [key, value] of Object.entries(secretMap)) {
if (matchesSchema(key, schema)) {
filteredMap[key] = value;
}
}
return filteredMap;
};
export const SecretSyncFns = { export const SecretSyncFns = {
syncSecrets: ( syncSecrets: (
@@ -105,49 +126,51 @@ export const SecretSyncFns = {
secretMap: TSecretMap, secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => { ): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap); const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) { switch (secretSync.destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); return AwsParameterStoreSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap); return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, secretMap); return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap); return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({ return azureKeyVaultSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration: case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({ return azureAppConfigurationSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks: case SecretSync.Databricks:
return databricksSyncFactory({ return databricksSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec: case SecretSync.Humanitec:
return HumanitecSyncFns.syncSecrets(secretSync, secretMap); return HumanitecSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud: case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap); return TerraformCloudSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda: case SecretSync.Camunda:
return camundaSyncFactory({ return camundaSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).syncSecrets(secretSync, secretMap); }).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel: case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap); return VercelSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill: case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, secretMap); return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault: case SecretSync.HCVault:
return HCVaultSyncFns.syncSecrets(secretSync, secretMap); return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity: case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, secretMap); return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -213,63 +236,67 @@ export const SecretSyncFns = {
case SecretSync.TeamCity: case SecretSync.TeamCity:
secretMap = await TeamCitySyncFns.getSecrets(secretSync); secretMap = await TeamCitySyncFns.getSecrets(secretSync);
break; break;
case SecretSync.OCIVault:
secretMap = await OCIVaultSyncFns.getSecrets(secretSync);
break;
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
); );
} }
return secretMap; return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema);
// return stripAffixes(secretSync, secretMap);
}, },
removeSecrets: ( removeSecrets: (
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap, secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps { kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => { ): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap); const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) { switch (secretSync.destination) {
case SecretSync.AWSParameterStore: case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); return AwsParameterStoreSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager: case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap); return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub: case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, secretMap); return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager: case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap); return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault: case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({ return azureKeyVaultSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration: case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({ return azureAppConfigurationSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks: case SecretSync.Databricks:
return databricksSyncFactory({ return databricksSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec: case SecretSync.Humanitec:
return HumanitecSyncFns.removeSecrets(secretSync, secretMap); return HumanitecSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud: case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap); return TerraformCloudSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda: case SecretSync.Camunda:
return camundaSyncFactory({ return camundaSyncFactory({
appConnectionDAL, appConnectionDAL,
kmsService kmsService
}).removeSecrets(secretSync, secretMap); }).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel: case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap); return VercelSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill: case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, secretMap); return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault: case SecretSync.HCVault:
return HCVaultSyncFns.removeSecrets(secretSync, secretMap); return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity: case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, secretMap); return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `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.Vercel]: "Vercel",
[SecretSync.Windmill]: "Windmill", [SecretSync.Windmill]: "Windmill",
[SecretSync.HCVault]: "Hashicorp Vault", [SecretSync.HCVault]: "Hashicorp Vault",
[SecretSync.TeamCity]: "TeamCity" [SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault"
}; };
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = { 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.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill, [SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.HCVault]: AppConnection.HCVault,
[SecretSync.TeamCity]: AppConnection.TeamCity [SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI
}; };
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { AnyZodObject, z } from "zod"; import { AnyZodObject, z } from "zod";
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs"; import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
@@ -24,6 +25,14 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
? z.nativeEnum(SecretSyncInitialSyncBehavior) ? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior), ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
keySchema: z
.string()
.optional()
.refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), {
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
})
.describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema),
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
}); });
@@ -67,6 +67,7 @@ import {
THumanitecSyncListItem, THumanitecSyncListItem,
THumanitecSyncWithCredentials THumanitecSyncWithCredentials
} from "./humanitec"; } from "./humanitec";
import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault";
import { import {
TTeamCitySync, TTeamCitySync,
TTeamCitySyncInput, TTeamCitySyncInput,
@@ -95,7 +96,8 @@ export type TSecretSync =
| TVercelSync | TVercelSync
| TWindmillSync | TWindmillSync
| THCVaultSync | THCVaultSync
| TTeamCitySync; | TTeamCitySync
| TOCIVaultSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@@ -111,7 +113,8 @@ export type TSecretSyncWithCredentials =
| TVercelSyncWithCredentials | TVercelSyncWithCredentials
| TWindmillSyncWithCredentials | TWindmillSyncWithCredentials
| THCVaultSyncWithCredentials | THCVaultSyncWithCredentials
| TTeamCitySyncWithCredentials; | TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@@ -127,7 +130,8 @@ export type TSecretSyncInput =
| TVercelSyncInput | TVercelSyncInput
| TWindmillSyncInput | TWindmillSyncInput
| THCVaultSyncInput | THCVaultSyncInput
| TTeamCitySyncInput; | TTeamCitySyncInput
| TOCIVaultSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@@ -143,7 +147,8 @@ export type TSecretSyncListItem =
| TVercelSyncListItem | TVercelSyncListItem
| TWindmillSyncListItem | TWindmillSyncListItem
| THCVaultSyncListItem | THCVaultSyncListItem
| TTeamCitySyncListItem; | TTeamCitySyncListItem
| TOCIVaultSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;
@@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity"; import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; 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"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { import {
TDeleteTeamCityVariable, TDeleteTeamCityVariable,
@@ -125,6 +126,9 @@ export const TeamCitySyncFns = {
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
for await (const [key, variable] of Object.entries(variables)) { for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!(key in secretMap)) { if (!(key in secretMap)) {
try { try {
await deleteTeamCityVariable({ await deleteTeamCityVariable({
@@ -4,6 +4,7 @@ import { AxiosResponse } from "axios";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; 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"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
@@ -231,6 +232,9 @@ export const TerraformCloudSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return; if (secretSync.syncOptions.disableSecretDeletion) return;
for (const terraformCloudVariable of terraformCloudVariables) { for (const terraformCloudVariable of terraformCloudVariables) {
// eslint-disable-next-line no-continue
if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue;
if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) { if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) {
await deleteVariable(secretSync, terraformCloudVariable); await deleteVariable(secretSync, terraformCloudVariable);
} }

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