mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-08 15:13:55 -05:00
Merge branch 'main' of https://github.com/Infisical/infisical into feat/suborg-scope-support
This commit is contained in:
3916
backend/package-lock.json
generated
3916
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -91,7 +91,7 @@
|
||||
"@babel/plugin-syntax-import-attributes": "^7.24.7",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@react-email/preview-server": "^4.3.0",
|
||||
"@react-email/preview-server": "^5.0.6",
|
||||
"@smithy/types": "^4.3.1",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
@@ -129,7 +129,7 @@
|
||||
"nodemon": "^3.0.2",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"prompt-sync": "^4.2.0",
|
||||
"react-email": "^4.3.0",
|
||||
"react-email": "^5.0.6",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.8",
|
||||
@@ -184,7 +184,7 @@
|
||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@peculiar/x509": "^1.12.1",
|
||||
"@react-email/components": "0.0.36",
|
||||
"@react-email/components": "^1.0.1",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@sindresorhus/slugify": "1.1.0",
|
||||
"@slack/oauth": "^3.0.2",
|
||||
@@ -267,4 +267,4 @@
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -55,6 +55,7 @@ import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||
import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service";
|
||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TApprovalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service";
|
||||
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||
@@ -361,6 +362,7 @@ declare module "fastify" {
|
||||
convertor: TConvertorServiceFactory;
|
||||
subOrganization: TSubOrgServiceFactory;
|
||||
pkiAlertV2: TPkiAlertV2ServiceFactory;
|
||||
approvalPolicy: TApprovalPolicyServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
74
backend/src/@types/knex.d.ts
vendored
74
backend/src/@types/knex.d.ts
vendored
@@ -26,6 +26,30 @@ import {
|
||||
TAppConnections,
|
||||
TAppConnectionsInsert,
|
||||
TAppConnectionsUpdate,
|
||||
TApprovalPolicies,
|
||||
TApprovalPoliciesInsert,
|
||||
TApprovalPoliciesUpdate,
|
||||
TApprovalPolicyStepApprovers,
|
||||
TApprovalPolicyStepApproversInsert,
|
||||
TApprovalPolicyStepApproversUpdate,
|
||||
TApprovalPolicySteps,
|
||||
TApprovalPolicyStepsInsert,
|
||||
TApprovalPolicyStepsUpdate,
|
||||
TApprovalRequestApprovals,
|
||||
TApprovalRequestApprovalsInsert,
|
||||
TApprovalRequestApprovalsUpdate,
|
||||
TApprovalRequestGrants,
|
||||
TApprovalRequestGrantsInsert,
|
||||
TApprovalRequestGrantsUpdate,
|
||||
TApprovalRequests,
|
||||
TApprovalRequestsInsert,
|
||||
TApprovalRequestStepEligibleApprovers,
|
||||
TApprovalRequestStepEligibleApproversInsert,
|
||||
TApprovalRequestStepEligibleApproversUpdate,
|
||||
TApprovalRequestSteps,
|
||||
TApprovalRequestStepsInsert,
|
||||
TApprovalRequestStepsUpdate,
|
||||
TApprovalRequestsUpdate,
|
||||
TAuditLogs,
|
||||
TAuditLogsInsert,
|
||||
TAuditLogStreams,
|
||||
@@ -573,16 +597,16 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TCertificateRequests,
|
||||
TCertificateRequestsInsert,
|
||||
TCertificateRequestsUpdate
|
||||
} from "@app/db/schemas/certificate-requests";
|
||||
import {
|
||||
TAccessApprovalPoliciesEnvironments,
|
||||
TAccessApprovalPoliciesEnvironmentsInsert,
|
||||
TAccessApprovalPoliciesEnvironmentsUpdate
|
||||
} from "@app/db/schemas/access-approval-policies-environments";
|
||||
import {
|
||||
TCertificateRequests,
|
||||
TCertificateRequestsInsert,
|
||||
TCertificateRequestsUpdate
|
||||
} from "@app/db/schemas/certificate-requests";
|
||||
import {
|
||||
TIdentityAuthTemplates,
|
||||
TIdentityAuthTemplatesInsert,
|
||||
@@ -1475,5 +1499,45 @@ declare module "knex/types/tables" {
|
||||
TVaultExternalMigrationConfigsInsert,
|
||||
TVaultExternalMigrationConfigsUpdate
|
||||
>;
|
||||
[TableName.ApprovalPolicies]: KnexOriginal.CompositeTableType<
|
||||
TApprovalPolicies,
|
||||
TApprovalPoliciesInsert,
|
||||
TApprovalPoliciesUpdate
|
||||
>;
|
||||
[TableName.ApprovalPolicyStepApprovers]: KnexOriginal.CompositeTableType<
|
||||
TApprovalPolicyStepApprovers,
|
||||
TApprovalPolicyStepApproversInsert,
|
||||
TApprovalPolicyStepApproversUpdate
|
||||
>;
|
||||
[TableName.ApprovalPolicySteps]: KnexOriginal.CompositeTableType<
|
||||
TApprovalPolicySteps,
|
||||
TApprovalPolicyStepsInsert,
|
||||
TApprovalPolicyStepsUpdate
|
||||
>;
|
||||
[TableName.ApprovalRequestApprovals]: KnexOriginal.CompositeTableType<
|
||||
TApprovalRequestApprovals,
|
||||
TApprovalRequestApprovalsInsert,
|
||||
TApprovalRequestApprovalsUpdate
|
||||
>;
|
||||
[TableName.ApprovalRequestGrants]: KnexOriginal.CompositeTableType<
|
||||
TApprovalRequestGrants,
|
||||
TApprovalRequestGrantsInsert,
|
||||
TApprovalRequestGrantsUpdate
|
||||
>;
|
||||
[TableName.ApprovalRequestStepEligibleApprovers]: KnexOriginal.CompositeTableType<
|
||||
TApprovalRequestStepEligibleApprovers,
|
||||
TApprovalRequestStepEligibleApproversInsert,
|
||||
TApprovalRequestStepEligibleApproversUpdate
|
||||
>;
|
||||
[TableName.ApprovalRequestSteps]: KnexOriginal.CompositeTableType<
|
||||
TApprovalRequestSteps,
|
||||
TApprovalRequestStepsInsert,
|
||||
TApprovalRequestStepsUpdate
|
||||
>;
|
||||
[TableName.ApprovalRequests]: KnexOriginal.CompositeTableType<
|
||||
TApprovalRequests,
|
||||
TApprovalRequestsInsert,
|
||||
TApprovalRequestsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
194
backend/src/db/migrations/20251203002657_global-approvals.ts
Normal file
194
backend/src/db/migrations/20251203002657_global-approvals.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
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.ApprovalPolicies))) {
|
||||
await knex.schema.createTable(TableName.ApprovalPolicies, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable().index();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
t.uuid("organizationId").notNullable().index();
|
||||
t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
t.string("type").notNullable().index();
|
||||
t.string("name").notNullable();
|
||||
|
||||
t.boolean("isActive").defaultTo(true);
|
||||
|
||||
t.string("maxRequestTtl").nullable(); // 1hour, 30seconds, etc
|
||||
|
||||
t.jsonb("conditions").notNullable();
|
||||
t.jsonb("constraints").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.ApprovalPolicies);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalPolicySteps))) {
|
||||
await knex.schema.createTable(TableName.ApprovalPolicySteps, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("policyId").notNullable().index();
|
||||
t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("CASCADE");
|
||||
|
||||
t.string("name").nullable();
|
||||
t.integer("stepNumber").notNullable();
|
||||
|
||||
t.integer("requiredApprovals").notNullable();
|
||||
t.boolean("notifyApprovers").defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalPolicyStepApprovers))) {
|
||||
await knex.schema.createTable(TableName.ApprovalPolicyStepApprovers, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("policyStepId").notNullable().index();
|
||||
t.foreign("policyStepId").references("id").inTable(TableName.ApprovalPolicySteps).onDelete("CASCADE");
|
||||
|
||||
t.uuid("userId").nullable().index();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.uuid("groupId").nullable().index();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
|
||||
t.check('("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)');
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalRequests))) {
|
||||
await knex.schema.createTable(TableName.ApprovalRequests, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable().index();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
t.uuid("organizationId").notNullable().index();
|
||||
t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
t.uuid("policyId").nullable().index();
|
||||
t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("SET NULL");
|
||||
|
||||
t.uuid("requesterId").nullable().index();
|
||||
t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
|
||||
// To be used in the event of requester deletion
|
||||
t.string("requesterName").notNullable();
|
||||
t.string("requesterEmail").notNullable();
|
||||
|
||||
t.string("type").notNullable().index();
|
||||
|
||||
t.string("status").notNullable().index();
|
||||
t.text("justification").nullable();
|
||||
t.integer("currentStep").notNullable();
|
||||
|
||||
t.jsonb("requestData").notNullable();
|
||||
|
||||
t.timestamp("expiresAt").nullable();
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.ApprovalRequests);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalRequestSteps))) {
|
||||
await knex.schema.createTable(TableName.ApprovalRequestSteps, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("requestId").notNullable().index();
|
||||
t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("CASCADE");
|
||||
|
||||
t.integer("stepNumber").notNullable();
|
||||
|
||||
t.string("name").nullable();
|
||||
t.string("status").notNullable().index();
|
||||
|
||||
t.integer("requiredApprovals").notNullable();
|
||||
t.boolean("notifyApprovers").defaultTo(false);
|
||||
|
||||
t.timestamp("startedAt").nullable();
|
||||
t.timestamp("completedAt").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalRequestStepEligibleApprovers))) {
|
||||
await knex.schema.createTable(TableName.ApprovalRequestStepEligibleApprovers, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("stepId").notNullable().index();
|
||||
t.foreign("stepId").references("id").inTable(TableName.ApprovalRequestSteps).onDelete("CASCADE");
|
||||
|
||||
t.uuid("userId").nullable().index();
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.uuid("groupId").nullable().index();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
|
||||
t.check('("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)');
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalRequestApprovals))) {
|
||||
await knex.schema.createTable(TableName.ApprovalRequestApprovals, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("stepId").notNullable().index();
|
||||
t.foreign("stepId").references("id").inTable(TableName.ApprovalRequestSteps).onDelete("CASCADE");
|
||||
|
||||
t.uuid("approverUserId").notNullable().index();
|
||||
t.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
|
||||
t.string("decision").notNullable();
|
||||
t.text("comment").nullable();
|
||||
|
||||
t.timestamp("createdAt").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ApprovalRequestGrants))) {
|
||||
await knex.schema.createTable(TableName.ApprovalRequestGrants, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable().index();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
t.uuid("requestId").nullable().index();
|
||||
t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("SET NULL");
|
||||
|
||||
t.uuid("granteeUserId").nullable().index();
|
||||
t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
|
||||
t.uuid("revokedByUserId").nullable().index();
|
||||
t.foreign("revokedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||
|
||||
t.text("revocationReason").nullable();
|
||||
|
||||
t.string("status").notNullable().index();
|
||||
t.string("type").notNullable().index();
|
||||
|
||||
t.jsonb("attributes").notNullable();
|
||||
|
||||
t.timestamp("createdAt").defaultTo(knex.fn.now());
|
||||
t.timestamp("expiresAt").nullable();
|
||||
t.timestamp("revokedAt").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalRequestGrants);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalRequestApprovals);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalRequestStepEligibleApprovers);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalRequestSteps);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalRequests);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalPolicyStepApprovers);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalPolicySteps);
|
||||
await knex.schema.dropTableIfExists(TableName.ApprovalPolicies);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.ApprovalRequests);
|
||||
await dropOnUpdateTrigger(knex, TableName.ApprovalPolicies);
|
||||
}
|
||||
21
backend/src/db/migrations/20251203224427_pam-aws-console.ts
Normal file
21
backend/src/db/migrations/20251203224427_pam-aws-console.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGatewayId = await knex.schema.hasColumn(TableName.PamResource, "gatewayId");
|
||||
if (hasGatewayId) {
|
||||
await knex.schema.alterTable(TableName.PamResource, (t) => {
|
||||
t.uuid("gatewayId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGatewayId = await knex.schema.hasColumn(TableName.PamResource, "gatewayId");
|
||||
if (hasGatewayId) {
|
||||
await knex.schema.alterTable(TableName.PamResource, (t) => {
|
||||
t.uuid("gatewayId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
26
backend/src/db/schemas/approval-policies.ts
Normal file
26
backend/src/db/schemas/approval-policies.ts
Normal file
@@ -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 ApprovalPoliciesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
organizationId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
isActive: z.boolean().default(true).nullable().optional(),
|
||||
maxRequestTtl: z.string().nullable().optional(),
|
||||
conditions: z.unknown(),
|
||||
constraints: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TApprovalPolicies = z.infer<typeof ApprovalPoliciesSchema>;
|
||||
export type TApprovalPoliciesInsert = Omit<z.input<typeof ApprovalPoliciesSchema>, TImmutableDBKeys>;
|
||||
export type TApprovalPoliciesUpdate = Partial<Omit<z.input<typeof ApprovalPoliciesSchema>, TImmutableDBKeys>>;
|
||||
24
backend/src/db/schemas/approval-policy-step-approvers.ts
Normal file
24
backend/src/db/schemas/approval-policy-step-approvers.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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 ApprovalPolicyStepApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyStepId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
groupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TApprovalPolicyStepApprovers = z.infer<typeof ApprovalPolicyStepApproversSchema>;
|
||||
export type TApprovalPolicyStepApproversInsert = Omit<
|
||||
z.input<typeof ApprovalPolicyStepApproversSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TApprovalPolicyStepApproversUpdate = Partial<
|
||||
Omit<z.input<typeof ApprovalPolicyStepApproversSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
21
backend/src/db/schemas/approval-policy-steps.ts
Normal file
21
backend/src/db/schemas/approval-policy-steps.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 ApprovalPolicyStepsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
policyId: z.string().uuid(),
|
||||
name: z.string().nullable().optional(),
|
||||
stepNumber: z.number(),
|
||||
requiredApprovals: z.number(),
|
||||
notifyApprovers: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TApprovalPolicySteps = z.infer<typeof ApprovalPolicyStepsSchema>;
|
||||
export type TApprovalPolicyStepsInsert = Omit<z.input<typeof ApprovalPolicyStepsSchema>, TImmutableDBKeys>;
|
||||
export type TApprovalPolicyStepsUpdate = Partial<Omit<z.input<typeof ApprovalPolicyStepsSchema>, TImmutableDBKeys>>;
|
||||
23
backend/src/db/schemas/approval-request-approvals.ts
Normal file
23
backend/src/db/schemas/approval-request-approvals.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ApprovalRequestApprovalsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
stepId: z.string().uuid(),
|
||||
approverUserId: z.string().uuid(),
|
||||
decision: z.string(),
|
||||
comment: z.string().nullable().optional(),
|
||||
createdAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TApprovalRequestApprovals = z.infer<typeof ApprovalRequestApprovalsSchema>;
|
||||
export type TApprovalRequestApprovalsInsert = Omit<z.input<typeof ApprovalRequestApprovalsSchema>, TImmutableDBKeys>;
|
||||
export type TApprovalRequestApprovalsUpdate = Partial<
|
||||
Omit<z.input<typeof ApprovalRequestApprovalsSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
27
backend/src/db/schemas/approval-request-grants.ts
Normal file
27
backend/src/db/schemas/approval-request-grants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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 ApprovalRequestGrantsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
requestId: z.string().uuid().nullable().optional(),
|
||||
granteeUserId: z.string().uuid().nullable().optional(),
|
||||
revokedByUserId: z.string().uuid().nullable().optional(),
|
||||
revocationReason: z.string().nullable().optional(),
|
||||
status: z.string(),
|
||||
type: z.string(),
|
||||
attributes: z.unknown(),
|
||||
createdAt: z.date().nullable().optional(),
|
||||
expiresAt: z.date().nullable().optional(),
|
||||
revokedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TApprovalRequestGrants = z.infer<typeof ApprovalRequestGrantsSchema>;
|
||||
export type TApprovalRequestGrantsInsert = Omit<z.input<typeof ApprovalRequestGrantsSchema>, TImmutableDBKeys>;
|
||||
export type TApprovalRequestGrantsUpdate = Partial<Omit<z.input<typeof ApprovalRequestGrantsSchema>, TImmutableDBKeys>>;
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 ApprovalRequestStepEligibleApproversSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
stepId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
groupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TApprovalRequestStepEligibleApprovers = z.infer<typeof ApprovalRequestStepEligibleApproversSchema>;
|
||||
export type TApprovalRequestStepEligibleApproversInsert = Omit<
|
||||
z.input<typeof ApprovalRequestStepEligibleApproversSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TApprovalRequestStepEligibleApproversUpdate = Partial<
|
||||
Omit<z.input<typeof ApprovalRequestStepEligibleApproversSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
24
backend/src/db/schemas/approval-request-steps.ts
Normal file
24
backend/src/db/schemas/approval-request-steps.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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 ApprovalRequestStepsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
requestId: z.string().uuid(),
|
||||
stepNumber: z.number(),
|
||||
name: z.string().nullable().optional(),
|
||||
status: z.string(),
|
||||
requiredApprovals: z.number(),
|
||||
notifyApprovers: z.boolean().default(false).nullable().optional(),
|
||||
startedAt: z.date().nullable().optional(),
|
||||
completedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TApprovalRequestSteps = z.infer<typeof ApprovalRequestStepsSchema>;
|
||||
export type TApprovalRequestStepsInsert = Omit<z.input<typeof ApprovalRequestStepsSchema>, TImmutableDBKeys>;
|
||||
export type TApprovalRequestStepsUpdate = Partial<Omit<z.input<typeof ApprovalRequestStepsSchema>, TImmutableDBKeys>>;
|
||||
30
backend/src/db/schemas/approval-requests.ts
Normal file
30
backend/src/db/schemas/approval-requests.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 ApprovalRequestsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
organizationId: z.string().uuid(),
|
||||
policyId: z.string().uuid().nullable().optional(),
|
||||
requesterId: z.string().uuid().nullable().optional(),
|
||||
requesterName: z.string(),
|
||||
requesterEmail: z.string(),
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
justification: z.string().nullable().optional(),
|
||||
currentStep: z.number(),
|
||||
requestData: z.unknown(),
|
||||
expiresAt: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TApprovalRequests = z.infer<typeof ApprovalRequestsSchema>;
|
||||
export type TApprovalRequestsInsert = Omit<z.input<typeof ApprovalRequestsSchema>, TImmutableDBKeys>;
|
||||
export type TApprovalRequestsUpdate = Partial<Omit<z.input<typeof ApprovalRequestsSchema>, TImmutableDBKeys>>;
|
||||
@@ -6,6 +6,14 @@ export * from "./access-approval-requests-reviewers";
|
||||
export * from "./additional-privileges";
|
||||
export * from "./api-keys";
|
||||
export * from "./app-connections";
|
||||
export * from "./approval-policies";
|
||||
export * from "./approval-policy-step-approvers";
|
||||
export * from "./approval-policy-steps";
|
||||
export * from "./approval-request-approvals";
|
||||
export * from "./approval-request-grants";
|
||||
export * from "./approval-request-step-eligible-approvers";
|
||||
export * from "./approval-request-steps";
|
||||
export * from "./approval-requests";
|
||||
export * from "./audit-log-streams";
|
||||
export * from "./audit-logs";
|
||||
export * from "./auth-token-sessions";
|
||||
|
||||
@@ -223,7 +223,17 @@ export enum TableName {
|
||||
PkiAcmeOrder = "pki_acme_orders",
|
||||
PkiAcmeOrderAuth = "pki_acme_order_auths",
|
||||
PkiAcmeAuth = "pki_acme_auths",
|
||||
PkiAcmeChallenge = "pki_acme_challenges"
|
||||
PkiAcmeChallenge = "pki_acme_challenges",
|
||||
|
||||
// Approval Policies
|
||||
ApprovalPolicies = "approval_policies",
|
||||
ApprovalPolicySteps = "approval_policy_steps",
|
||||
ApprovalPolicyStepApprovers = "approval_policy_step_approvers",
|
||||
ApprovalRequests = "approval_requests",
|
||||
ApprovalRequestSteps = "approval_request_steps",
|
||||
ApprovalRequestStepEligibleApprovers = "approval_request_step_eligible_approvers",
|
||||
ApprovalRequestApprovals = "approval_request_approvals",
|
||||
ApprovalRequestGrants = "approval_request_grants"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
||||
|
||||
@@ -13,7 +13,7 @@ export const PamResourcesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
gatewayId: z.string().uuid(),
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
resourceType: z.string(),
|
||||
encryptedConnectionDetails: zodBuffer,
|
||||
createdAt: z.date(),
|
||||
|
||||
@@ -4,15 +4,10 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
ExternalKmsGcpCredentialSchema,
|
||||
ExternalKmsGcpSchema,
|
||||
ExternalKmsInputSchema,
|
||||
ExternalKmsInputUpdateSchema,
|
||||
KmsGcpKeyFetchAuthType,
|
||||
KmsProviders,
|
||||
TExternalKmsGcpCredentialSchema
|
||||
ExternalKmsInputUpdateSchema
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
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";
|
||||
@@ -293,67 +288,4 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/gcp/keys",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.discriminatedUnion("authMethod", [
|
||||
z.object({
|
||||
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
|
||||
region: z.string().trim().min(1),
|
||||
credential: ExternalKmsGcpCredentialSchema
|
||||
}),
|
||||
z.object({
|
||||
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
|
||||
region: z.string().trim().min(1),
|
||||
kmsId: z.string().trim().min(1)
|
||||
})
|
||||
]),
|
||||
response: {
|
||||
200: z.object({
|
||||
keys: z.string().array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { region, authMethod } = req.body;
|
||||
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
|
||||
|
||||
if (authMethod === KmsGcpKeyFetchAuthType.Credential) {
|
||||
credentialJson = req.body.credential;
|
||||
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms) {
|
||||
const externalKms = await server.services.externalKms.findById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.kmsId
|
||||
});
|
||||
|
||||
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
|
||||
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
|
||||
}
|
||||
|
||||
credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema;
|
||||
}
|
||||
|
||||
if (!credentialJson) {
|
||||
throw new NotFoundError({
|
||||
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
|
||||
});
|
||||
}
|
||||
|
||||
const results = await server.services.externalKms.fetchGcpKeys({
|
||||
credential: credentialJson,
|
||||
gcpRegion: region
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ExternalKmsAwsSchema, KmsProviders } from "@app/ee/services/external-kms/providers/model";
|
||||
|
||||
import { registerExternalKmsEndpoints } from "./external-kms-endpoints";
|
||||
|
||||
export const registerAwsKmsRouter = async (server: FastifyZodProvider) => {
|
||||
registerExternalKmsEndpoints({
|
||||
server,
|
||||
provider: KmsProviders.Aws,
|
||||
createSchema: ExternalKmsAwsSchema,
|
||||
updateSchema: ExternalKmsAwsSchema.partial()
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,288 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
KmsProviders,
|
||||
SanitizedExternalKmsAwsSchema,
|
||||
SanitizedExternalKmsGcpSchema,
|
||||
TExternalKmsInputSchema,
|
||||
TExternalKmsInputUpdateSchema
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
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";
|
||||
|
||||
const sanitizedExternalSchema = KmsKeysSchema.extend({
|
||||
externalKms: ExternalKmsSchema.pick({
|
||||
id: true,
|
||||
status: true,
|
||||
statusDetails: true,
|
||||
provider: true
|
||||
}).extend({
|
||||
configuration: z.union([SanitizedExternalKmsAwsSchema, SanitizedExternalKmsGcpSchema]),
|
||||
credentialsHash: z.string().optional()
|
||||
})
|
||||
});
|
||||
|
||||
export const registerExternalKmsEndpoints = <
|
||||
T extends { type: KmsProviders; inputs: TExternalKmsInputSchema["inputs"] }
|
||||
>({
|
||||
server,
|
||||
provider,
|
||||
createSchema,
|
||||
updateSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
provider: T["type"];
|
||||
createSchema: z.ZodType<T["inputs"]>;
|
||||
updateSchema: z.ZodType<Partial<T["inputs"]>>;
|
||||
}) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedExternalSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.findById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
// Validate that the KMS is of the expected provider type
|
||||
if (externalKms.external.provider !== provider) {
|
||||
throw new BadRequestError({
|
||||
message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}`
|
||||
});
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
name: externalKms.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
external: { providerInput: configuration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.digest("hex");
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().min(1).trim().toLowerCase(),
|
||||
description: z.string().trim().optional(),
|
||||
configuration: createSchema
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedExternalSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, description, configuration } = req.body as {
|
||||
name: string;
|
||||
description?: string;
|
||||
configuration: T["inputs"];
|
||||
};
|
||||
|
||||
const providerInput = {
|
||||
type: provider,
|
||||
inputs: configuration
|
||||
} as TExternalKmsInputSchema;
|
||||
|
||||
const externalKms = await server.services.externalKms.create({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name,
|
||||
provider: providerInput,
|
||||
description
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider,
|
||||
name,
|
||||
description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
external: { providerInput: externalKmsConfiguration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.digest("hex");
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().min(1).trim().toLowerCase().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
configuration: updateSchema.optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedExternalSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { name, description, configuration } = req.body as {
|
||||
name?: string;
|
||||
description?: string;
|
||||
configuration: Partial<T["inputs"]>;
|
||||
};
|
||||
|
||||
const providerInput = {
|
||||
type: provider,
|
||||
inputs: configuration
|
||||
} as TExternalKmsInputUpdateSchema;
|
||||
|
||||
const externalKms = await server.services.externalKms.updateById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name,
|
||||
provider: providerInput,
|
||||
description,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider,
|
||||
name,
|
||||
description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
external: { providerInput: externalKmsConfiguration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.digest("hex");
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedExternalSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.deleteById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
// Validate that the KMS is of the expected provider type
|
||||
if (externalKms.external.provider !== provider) {
|
||||
throw new BadRequestError({
|
||||
message: `KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}`
|
||||
});
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
name: externalKms.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
external: { providerInput: configuration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.digest("hex");
|
||||
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
ExternalKmsGcpCredentialSchema,
|
||||
ExternalKmsGcpSchema,
|
||||
KmsGcpKeyFetchAuthType,
|
||||
KmsProviders,
|
||||
TExternalKmsGcpCredentialSchema
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerExternalKmsEndpoints } from "./external-kms-endpoints";
|
||||
|
||||
export const registerGcpKmsRouter = async (server: FastifyZodProvider) => {
|
||||
registerExternalKmsEndpoints({
|
||||
server,
|
||||
provider: KmsProviders.Gcp,
|
||||
createSchema: ExternalKmsGcpSchema,
|
||||
updateSchema: ExternalKmsGcpSchema.partial()
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/keys",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.discriminatedUnion("authMethod", [
|
||||
z.object({
|
||||
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
|
||||
region: z.string().trim().min(1),
|
||||
credential: ExternalKmsGcpCredentialSchema
|
||||
}),
|
||||
z.object({
|
||||
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
|
||||
region: z.string().trim().min(1),
|
||||
kmsId: z.string().trim().min(1)
|
||||
})
|
||||
]),
|
||||
response: {
|
||||
200: z.object({
|
||||
keys: z.string().array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { region, authMethod } = req.body;
|
||||
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
|
||||
|
||||
if (authMethod === KmsGcpKeyFetchAuthType.Credential && "credential" in req.body) {
|
||||
credentialJson = req.body.credential;
|
||||
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms && "kmsId" in req.body) {
|
||||
const externalKms = await server.services.externalKms.findById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.kmsId
|
||||
});
|
||||
|
||||
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
|
||||
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
|
||||
}
|
||||
|
||||
const providerInput = externalKms.external.providerInput as { credential: TExternalKmsGcpCredentialSchema };
|
||||
credentialJson = providerInput.credential;
|
||||
}
|
||||
|
||||
if (!credentialJson) {
|
||||
throw new NotFoundError({
|
||||
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
|
||||
});
|
||||
}
|
||||
|
||||
const results = await server.services.externalKms.fetchGcpKeys({
|
||||
credential: credentialJson,
|
||||
gcpRegion: region
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
});
|
||||
};
|
||||
9
backend/src/ee/routes/v1/external-kms-routers/index.ts
Normal file
9
backend/src/ee/routes/v1/external-kms-routers/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { KmsProviders } from "@app/ee/services/external-kms/providers/model";
|
||||
|
||||
import { registerAwsKmsRouter } from "./aws-kms-router";
|
||||
import { registerGcpKmsRouter } from "./gcp-kms-router";
|
||||
|
||||
export const EXTERNAL_KMS_REGISTER_ROUTER_MAP: Record<KmsProviders, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[KmsProviders.Aws]: registerAwsKmsRouter,
|
||||
[KmsProviders.Gcp]: registerGcpKmsRouter
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"
|
||||
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { EXTERNAL_KMS_REGISTER_ROUTER_MAP } from "./external-kms-routers";
|
||||
import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
@@ -162,9 +163,19 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/additional-privilege" }
|
||||
);
|
||||
|
||||
await server.register(registerExternalKmsRouter, {
|
||||
prefix: "/external-kms"
|
||||
});
|
||||
await server.register(
|
||||
async (externalKmsRouter) => {
|
||||
await externalKmsRouter.register(registerExternalKmsRouter);
|
||||
|
||||
// Provider-specific endpoints
|
||||
await Promise.all(
|
||||
Object.entries(EXTERNAL_KMS_REGISTER_ROUTER_MAP).map(([provider, router]) =>
|
||||
externalKmsRouter.register(router, { prefix: `/${provider}` })
|
||||
)
|
||||
);
|
||||
},
|
||||
{ prefix: "/external-kms" }
|
||||
);
|
||||
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
|
||||
|
||||
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateAwsIamAccountSchema,
|
||||
SanitizedAwsIamAccountWithResourceSchema,
|
||||
UpdateAwsIamAccountSchema
|
||||
} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas";
|
||||
import {
|
||||
CreateMySQLAccountSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema,
|
||||
@@ -44,5 +49,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
||||
createAccountSchema: CreateSSHAccountSchema,
|
||||
updateAccountSchema: UpdateSSHAccountSchema
|
||||
});
|
||||
},
|
||||
[PamResource.AwsIam]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.AwsIam,
|
||||
accountResponseSchema: SanitizedAwsIamAccountWithResourceSchema,
|
||||
createAccountSchema: CreateAwsIamAccountSchema,
|
||||
updateAccountSchema: UpdateAwsIamAccountSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const registerPamResourceEndpoints = <C extends TPamAccount>({
|
||||
folderId?: C["folderId"];
|
||||
name: C["name"];
|
||||
description?: C["description"];
|
||||
rotationEnabled: C["rotationEnabled"];
|
||||
rotationEnabled?: C["rotationEnabled"];
|
||||
rotationIntervalSeconds?: C["rotationIntervalSeconds"];
|
||||
}>;
|
||||
updateAccountSchema: z.ZodType<{
|
||||
@@ -65,7 +65,7 @@ export const registerPamResourceEndpoints = <C extends TPamAccount>({
|
||||
folderId: req.body.folderId,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
rotationEnabled: req.body.rotationEnabled,
|
||||
rotationEnabled: req.body.rotationEnabled ?? false,
|
||||
rotationIntervalSeconds: req.body.rotationIntervalSeconds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { z } from "zod";
|
||||
import { PamFoldersSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-enums";
|
||||
import { SanitizedAwsIamAccountWithResourceSchema } from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas";
|
||||
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import { GatewayAccessResponseSchema } from "@app/ee/services/pam-resource/pam-resource-schemas";
|
||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import { SanitizedSSHAccountWithResourceSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -18,9 +20,12 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SanitizedAccountSchema = z.union([
|
||||
SanitizedSSHAccountWithResourceSchema, // ORDER MATTERS
|
||||
SanitizedPostgresAccountWithResourceSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema
|
||||
SanitizedMySQLAccountWithResourceSchema,
|
||||
SanitizedAwsIamAccountWithResourceSchema
|
||||
]);
|
||||
|
||||
type TSanitizedAccount = z.infer<typeof SanitizedAccountSchema>;
|
||||
|
||||
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@@ -93,7 +98,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
return { accounts, folders, totalCount, folderId, folderPaths };
|
||||
return { accounts: accounts as TSanitizedAccount[], folders, totalCount, folderId, folderPaths };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,7 +111,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "Access PAM account",
|
||||
body: z.object({
|
||||
accountId: z.string().uuid(),
|
||||
accountPath: z.string().trim(),
|
||||
projectId: z.string().uuid(),
|
||||
duration: z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -124,18 +130,19 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
sessionId: z.string(),
|
||||
resourceType: z.nativeEnum(PamResource),
|
||||
relayClientCertificate: z.string(),
|
||||
relayClientPrivateKey: z.string(),
|
||||
relayServerCertificateChain: z.string(),
|
||||
gatewayClientCertificate: z.string(),
|
||||
gatewayClientPrivateKey: z.string(),
|
||||
gatewayServerCertificateChain: z.string(),
|
||||
relayHost: z.string(),
|
||||
metadata: z.record(z.string(), z.string().optional()).optional()
|
||||
})
|
||||
200: z.discriminatedUnion("resourceType", [
|
||||
// Gateway-based resources (Postgres, MySQL, SSH)
|
||||
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.Postgres) }),
|
||||
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.MySQL) }),
|
||||
GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.SSH) }),
|
||||
// AWS IAM (no gateway, returns console URL)
|
||||
z.object({
|
||||
sessionId: z.string(),
|
||||
resourceType: z.literal(PamResource.AwsIam),
|
||||
consoleUrl: z.string().url(),
|
||||
metadata: z.record(z.string(), z.string().optional()).optional()
|
||||
})
|
||||
])
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
@@ -151,7 +158,9 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
actorIp: req.realIp,
|
||||
actorName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
|
||||
actorUserAgent: req.auditLogInfo.userAgent ?? "",
|
||||
...req.body
|
||||
accountPath: req.body.accountPath,
|
||||
projectId: req.body.projectId,
|
||||
duration: req.body.duration
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
@@ -159,11 +168,12 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: response.projectId,
|
||||
projectId: req.body.projectId,
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_ACCESS,
|
||||
metadata: {
|
||||
accountId: req.body.accountId,
|
||||
accountId: response.account.id,
|
||||
accountPath: req.body.accountPath,
|
||||
accountName: response.account.name,
|
||||
duration: req.body.duration ? new Date(req.body.duration).toISOString() : undefined
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateAwsIamResourceSchema,
|
||||
SanitizedAwsIamResourceSchema,
|
||||
UpdateAwsIamResourceSchema
|
||||
} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas";
|
||||
import {
|
||||
CreateMySQLResourceSchema,
|
||||
MySQLResourceSchema,
|
||||
@@ -44,5 +49,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
||||
createResourceSchema: CreateSSHResourceSchema,
|
||||
updateResourceSchema: UpdateSSHResourceSchema
|
||||
});
|
||||
},
|
||||
[PamResource.AwsIam]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.AwsIam,
|
||||
resourceResponseSchema: SanitizedAwsIamResourceSchema,
|
||||
createResourceSchema: CreateAwsIamResourceSchema,
|
||||
updateResourceSchema: UpdateAwsIamResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export const registerPamResourceEndpoints = <T extends TPamResource>({
|
||||
createResourceSchema: z.ZodType<{
|
||||
projectId: T["projectId"];
|
||||
connectionDetails: T["connectionDetails"];
|
||||
gatewayId: T["gatewayId"];
|
||||
gatewayId?: T["gatewayId"];
|
||||
name: T["name"];
|
||||
rotationAccountCredentials?: T["rotationAccountCredentials"];
|
||||
}>;
|
||||
@@ -103,7 +103,7 @@ export const registerPamResourceEndpoints = <T extends TPamResource>({
|
||||
type: EventType.PAM_RESOURCE_CREATE,
|
||||
metadata: {
|
||||
resourceType,
|
||||
gatewayId: req.body.gatewayId,
|
||||
...(req.body.gatewayId && { gatewayId: req.body.gatewayId }),
|
||||
name: req.body.name
|
||||
}
|
||||
}
|
||||
@@ -150,8 +150,8 @@ export const registerPamResourceEndpoints = <T extends TPamResource>({
|
||||
metadata: {
|
||||
resourceId: req.params.resourceId,
|
||||
resourceType,
|
||||
gatewayId: req.body.gatewayId,
|
||||
name: req.body.name
|
||||
...(req.body.gatewayId && { gatewayId: req.body.gatewayId }),
|
||||
...(req.body.name && { name: req.body.name })
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
AwsIamResourceListItemSchema,
|
||||
SanitizedAwsIamResourceSchema
|
||||
} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas";
|
||||
import {
|
||||
MySQLResourceListItemSchema,
|
||||
SanitizedMySQLResourceSchema
|
||||
@@ -22,13 +26,15 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SanitizedResourceSchema = z.union([
|
||||
SanitizedPostgresResourceSchema,
|
||||
SanitizedMySQLResourceSchema,
|
||||
SanitizedSSHResourceSchema
|
||||
SanitizedSSHResourceSchema,
|
||||
SanitizedAwsIamResourceSchema
|
||||
]);
|
||||
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||
PostgresResourceListItemSchema,
|
||||
MySQLResourceListItemSchema,
|
||||
SSHResourceListItemSchema
|
||||
SSHResourceListItemSchema,
|
||||
AwsIamResourceListItemSchema
|
||||
]);
|
||||
|
||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -315,6 +315,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
memberships: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
actorGroupId: z.string().nullish(),
|
||||
actorUserId: z.string().nullish(),
|
||||
roles: z
|
||||
.object({
|
||||
role: z.string()
|
||||
|
||||
@@ -84,7 +84,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
privilege: {
|
||||
...privilege,
|
||||
identityId: req.body.identityId,
|
||||
projectMembershipId: req.body.projectId,
|
||||
projectId: req.body.projectId,
|
||||
slug: privilege.name
|
||||
}
|
||||
@@ -168,7 +167,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
privilege: {
|
||||
...privilege,
|
||||
identityId: privilegeDoc.actorIdentityId as string,
|
||||
projectMembershipId: privilegeDoc.projectId as string,
|
||||
projectId: privilegeDoc.projectId as string,
|
||||
slug: privilege.name
|
||||
}
|
||||
@@ -222,7 +220,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
privilege: {
|
||||
...privilege,
|
||||
identityId: privilegeDoc.actorIdentityId as string,
|
||||
projectMembershipId: privilegeDoc.projectId as string,
|
||||
projectId: privilegeDoc.projectId as string,
|
||||
slug: privilege.name
|
||||
}
|
||||
@@ -276,7 +273,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
privilege: {
|
||||
...privilege,
|
||||
identityId: privilegeDoc.actorIdentityId as string,
|
||||
projectMembershipId: privilegeDoc.projectId as string,
|
||||
projectId: privilegeDoc.projectId as string,
|
||||
slug: privilege.name
|
||||
}
|
||||
@@ -339,7 +335,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
privilege: {
|
||||
...privilege,
|
||||
identityId: req.query.identityId,
|
||||
projectMembershipId: privilege.projectId as string,
|
||||
projectId,
|
||||
slug: privilege.name
|
||||
}
|
||||
@@ -391,7 +386,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
||||
privileges: privileges.map((privilege) => ({
|
||||
...privilege,
|
||||
identityId: req.query.identityId,
|
||||
projectMembershipId: privilege.projectId as string,
|
||||
projectId: req.query.projectId,
|
||||
slug: privilege.name
|
||||
}))
|
||||
|
||||
@@ -4,6 +4,7 @@ import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-r
|
||||
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
||||
import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router";
|
||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||
import { registerMongoDBCredentialsRotationRouter } from "./mongodb-credentials-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
|
||||
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
|
||||
@@ -26,5 +27,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
|
||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
||||
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter,
|
||||
[SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter
|
||||
[SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter,
|
||||
[SecretRotation.MongoDBCredentials]: registerMongoDBCredentialsRotationRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CreateMongoDBCredentialsRotationSchema,
|
||||
MongoDBCredentialsRotationGeneratedCredentialsSchema,
|
||||
MongoDBCredentialsRotationSchema,
|
||||
UpdateMongoDBCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/mongodb-credentials";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerMongoDBCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.MongoDBCredentials,
|
||||
server,
|
||||
responseSchema: MongoDBCredentialsRotationSchema,
|
||||
createSchema: CreateMongoDBCredentialsRotationSchema,
|
||||
updateSchema: UpdateMongoDBCredentialsRotationSchema,
|
||||
generatedCredentialsSchema: MongoDBCredentialsRotationGeneratedCredentialsSchema
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret
|
||||
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MongoDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mongodb-credentials";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
|
||||
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
|
||||
@@ -27,7 +28,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
OktaClientSecretRotationListItemSchema,
|
||||
RedisCredentialsRotationListItemSchema
|
||||
RedisCredentialsRotationListItemSchema,
|
||||
MongoDBCredentialsRotationListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -560,7 +560,21 @@ export enum EventType {
|
||||
PAM_RESOURCE_GET = "pam-resource-get",
|
||||
PAM_RESOURCE_CREATE = "pam-resource-create",
|
||||
PAM_RESOURCE_UPDATE = "pam-resource-update",
|
||||
PAM_RESOURCE_DELETE = "pam-resource-delete"
|
||||
PAM_RESOURCE_DELETE = "pam-resource-delete",
|
||||
APPROVAL_POLICY_CREATE = "approval-policy-create",
|
||||
APPROVAL_POLICY_UPDATE = "approval-policy-update",
|
||||
APPROVAL_POLICY_DELETE = "approval-policy-delete",
|
||||
APPROVAL_POLICY_LIST = "approval-policy-list",
|
||||
APPROVAL_POLICY_GET = "approval-policy-get",
|
||||
APPROVAL_REQUEST_GET = "approval-request-get",
|
||||
APPROVAL_REQUEST_LIST = "approval-request-list",
|
||||
APPROVAL_REQUEST_CREATE = "approval-request-create",
|
||||
APPROVAL_REQUEST_APPROVE = "approval-request-approve",
|
||||
APPROVAL_REQUEST_REJECT = "approval-request-reject",
|
||||
APPROVAL_REQUEST_CANCEL = "approval-request-cancel",
|
||||
APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list",
|
||||
APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get",
|
||||
APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -4086,6 +4100,7 @@ interface PamAccountAccessEvent {
|
||||
type: EventType.PAM_ACCOUNT_ACCESS;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
accountPath: string;
|
||||
accountName: string;
|
||||
duration?: string;
|
||||
};
|
||||
@@ -4168,7 +4183,7 @@ interface PamResourceCreateEvent {
|
||||
type: EventType.PAM_RESOURCE_CREATE;
|
||||
metadata: {
|
||||
resourceType: string;
|
||||
gatewayId: string;
|
||||
gatewayId?: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
@@ -4233,6 +4248,126 @@ interface GetCertificateFromRequestEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalPolicyCreateEvent {
|
||||
type: EventType.APPROVAL_POLICY_CREATE;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalPolicyUpdateEvent {
|
||||
type: EventType.APPROVAL_POLICY_UPDATE;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
policyId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalPolicyDeleteEvent {
|
||||
type: EventType.APPROVAL_POLICY_DELETE;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
policyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalPolicyListEvent {
|
||||
type: EventType.APPROVAL_POLICY_LIST;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalPolicyGetEvent {
|
||||
type: EventType.APPROVAL_POLICY_GET;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
policyId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestGetEvent {
|
||||
type: EventType.APPROVAL_REQUEST_GET;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
requestId: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestListEvent {
|
||||
type: EventType.APPROVAL_REQUEST_LIST;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestCreateEvent {
|
||||
type: EventType.APPROVAL_REQUEST_CREATE;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
justification?: string;
|
||||
requestDuration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestApproveEvent {
|
||||
type: EventType.APPROVAL_REQUEST_APPROVE;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
requestId: string;
|
||||
comment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestRejectEvent {
|
||||
type: EventType.APPROVAL_REQUEST_REJECT;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
requestId: string;
|
||||
comment?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestCancelEvent {
|
||||
type: EventType.APPROVAL_REQUEST_CANCEL;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
requestId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestGrantListEvent {
|
||||
type: EventType.APPROVAL_REQUEST_GRANT_LIST;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestGrantGetEvent {
|
||||
type: EventType.APPROVAL_REQUEST_GRANT_GET;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
grantId: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApprovalRequestGrantRevokeEvent {
|
||||
type: EventType.APPROVAL_REQUEST_GRANT_REVOKE;
|
||||
metadata: {
|
||||
policyType: string;
|
||||
grantId: string;
|
||||
revocationReason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| CreateSubOrganizationEvent
|
||||
| UpdateSubOrganizationEvent
|
||||
@@ -4619,4 +4754,18 @@ export type Event =
|
||||
| AutomatedRenewCertificateFailed
|
||||
| UserLoginEvent
|
||||
| SelectOrganizationEvent
|
||||
| SelectSubOrganizationEvent;
|
||||
| SelectSubOrganizationEvent
|
||||
| ApprovalPolicyCreateEvent
|
||||
| ApprovalPolicyUpdateEvent
|
||||
| ApprovalPolicyDeleteEvent
|
||||
| ApprovalPolicyListEvent
|
||||
| ApprovalPolicyGetEvent
|
||||
| ApprovalRequestGetEvent
|
||||
| ApprovalRequestListEvent
|
||||
| ApprovalRequestCreateEvent
|
||||
| ApprovalRequestApproveEvent
|
||||
| ApprovalRequestRejectEvent
|
||||
| ApprovalRequestCancelEvent
|
||||
| ApprovalRequestGrantListEvent
|
||||
| ApprovalRequestGrantGetEvent
|
||||
| ApprovalRequestGrantRevokeEvent;
|
||||
|
||||
@@ -24,7 +24,13 @@ import {
|
||||
} from "./external-kms-types";
|
||||
import { AwsKmsProviderFactory } from "./providers/aws-kms";
|
||||
import { GcpKmsProviderFactory } from "./providers/gcp-kms";
|
||||
import { ExternalKmsAwsSchema, ExternalKmsGcpSchema, KmsProviders, TExternalKmsGcpSchema } from "./providers/model";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
ExternalKmsGcpSchema,
|
||||
KmsProviders,
|
||||
TExternalKmsAwsSchema,
|
||||
TExternalKmsGcpSchema
|
||||
} from "./providers/model";
|
||||
|
||||
type TExternalKmsServiceFactoryDep = {
|
||||
externalKmsDAL: TExternalKmsDALFactory;
|
||||
@@ -72,6 +78,7 @@ export const externalKmsServiceFactory = ({
|
||||
const kmsName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
|
||||
switch (provider.type) {
|
||||
case KmsProviders.Aws:
|
||||
{
|
||||
@@ -88,9 +95,18 @@ export const externalKmsServiceFactory = ({
|
||||
try {
|
||||
// if missing kms key this generate a new kms key id and returns new provider input
|
||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||
sanitizedProviderInputObject = newProviderInput;
|
||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||
|
||||
await externalKms.validateConnection();
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection"
|
||||
});
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
@@ -101,7 +117,16 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
|
||||
try {
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInputObject = provider.inputs;
|
||||
sanitizedProviderInput = JSON.stringify(provider.inputs);
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection"
|
||||
});
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
@@ -139,7 +164,10 @@ export const externalKmsServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...kms, external: externalKmsCfg };
|
||||
return {
|
||||
...kms,
|
||||
external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject }
|
||||
};
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
@@ -179,6 +207,7 @@ export const externalKmsServiceFactory = ({
|
||||
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
let sanitizedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
|
||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
@@ -199,7 +228,16 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
|
||||
try {
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInputObject = updatedProviderInput;
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: error instanceof Error ? `AWS error: ${error.message}` : "Failed to validate AWS connection"
|
||||
});
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
@@ -214,7 +252,16 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput });
|
||||
try {
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInputObject = updatedProviderInput;
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to validate GCP connection"
|
||||
});
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
@@ -234,14 +281,17 @@ export const externalKmsServiceFactory = ({
|
||||
}
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.updateById(
|
||||
kmsDoc.id,
|
||||
{
|
||||
description,
|
||||
name: kmsName
|
||||
},
|
||||
tx
|
||||
);
|
||||
let kms = kmsDoc;
|
||||
if (kmsName || description) {
|
||||
kms = await kmsDAL.updateById(
|
||||
kmsDoc.id,
|
||||
{
|
||||
description,
|
||||
name: kmsName
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
if (encryptedProviderInputs) {
|
||||
const externalKmsCfg = await externalKmsDAL.updateById(
|
||||
externalKmsDoc.id,
|
||||
@@ -250,9 +300,9 @@ export const externalKmsServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...kms, external: externalKmsCfg };
|
||||
return { ...kms, external: { ...externalKmsCfg, providerInput: sanitizedProviderInputObject } };
|
||||
}
|
||||
return { ...kms, external: externalKmsDoc };
|
||||
return { ...kms, external: { ...externalKmsDoc, providerInput: sanitizedProviderInputObject } };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
@@ -273,9 +323,40 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new NotFoundError({ message: `External KMS with ID '${kmsId}' not found` });
|
||||
|
||||
let decryptedProviderInputObject: TExternalKmsAwsSchema | TExternalKmsGcpSchema;
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
switch (externalKmsDoc.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString())
|
||||
);
|
||||
decryptedProviderInputObject = decryptedProviderInput;
|
||||
break;
|
||||
}
|
||||
case KmsProviders.Gcp: {
|
||||
const decryptedProviderInput = await ExternalKmsGcpSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString())
|
||||
);
|
||||
|
||||
decryptedProviderInputObject = decryptedProviderInput;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
|
||||
return { ...kms, external: externalKmsDoc };
|
||||
return { ...kms, external: { ...externalKmsDoc, providerInput: decryptedProviderInputObject } };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
@@ -393,6 +474,14 @@ export const externalKmsServiceFactory = ({
|
||||
const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } });
|
||||
try {
|
||||
return await externalKms.getKeysList();
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: error instanceof Error ? `GCP error: ${error.message}` : "Failed to fetch GCP keys"
|
||||
});
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
|
||||
|
||||
@@ -22,7 +23,7 @@ const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
|
||||
});
|
||||
const response = await stsClient.send(command);
|
||||
if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey)
|
||||
throw new Error("Failed to assume role");
|
||||
throw new BadRequestError({ message: "Failed to assume role" });
|
||||
|
||||
const kmsClient = new KMSClient({
|
||||
region: providerInputs.awsRegion,
|
||||
@@ -67,7 +68,7 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro
|
||||
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
|
||||
const kmsKey = await awsClient.send(command);
|
||||
|
||||
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
|
||||
if (!kmsKey.KeyMetadata?.KeyId) throw new BadRequestError({ message: "Failed to generate kms key" });
|
||||
|
||||
const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
|
||||
...providerInputs,
|
||||
|
||||
@@ -19,27 +19,31 @@ export enum KmsGcpKeyFetchAuthType {
|
||||
Kms = "kmsId"
|
||||
}
|
||||
|
||||
const AwsConnectionAssumeRoleCredentialsSchema = z.object({
|
||||
assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"),
|
||||
externalId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("AWS assume role external id for further security in authentication")
|
||||
});
|
||||
|
||||
const AwsConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
|
||||
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
|
||||
});
|
||||
|
||||
export const ExternalKmsAwsSchema = z.object({
|
||||
credential: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AccessKey),
|
||||
data: z.object({
|
||||
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
|
||||
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
|
||||
})
|
||||
data: AwsConnectionAccessTokenCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AssumeRole),
|
||||
data: z.object({
|
||||
assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"),
|
||||
externalId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("AWS assume role external id for furthur security in authentication")
|
||||
})
|
||||
data: AwsConnectionAssumeRoleCredentialsSchema
|
||||
})
|
||||
])
|
||||
.describe("AWS credential information to connect"),
|
||||
@@ -52,6 +56,22 @@ export const ExternalKmsAwsSchema = z.object({
|
||||
});
|
||||
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
|
||||
|
||||
export const SanitizedExternalKmsAwsSchema = ExternalKmsAwsSchema.extend({
|
||||
credential: z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AccessKey),
|
||||
data: AwsConnectionAccessTokenCredentialsSchema.pick({ accessKey: true })
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AssumeRole),
|
||||
data: AwsConnectionAssumeRoleCredentialsSchema.pick({
|
||||
assumeRoleArn: true,
|
||||
externalId: true
|
||||
})
|
||||
})
|
||||
])
|
||||
});
|
||||
|
||||
export const ExternalKmsGcpCredentialSchema = z.object({
|
||||
type: z.literal(KmsGcpCredentialType.ServiceAccount),
|
||||
project_id: z.string().min(1),
|
||||
@@ -75,6 +95,8 @@ export const ExternalKmsGcpSchema = z.object({
|
||||
});
|
||||
export type TExternalKmsGcpSchema = z.infer<typeof ExternalKmsGcpSchema>;
|
||||
|
||||
export const SanitizedExternalKmsGcpSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true });
|
||||
|
||||
const ExternalKmsGcpClientSchema = ExternalKmsGcpSchema.pick({ gcpRegion: true }).extend({
|
||||
credential: ExternalKmsGcpCredentialSchema
|
||||
});
|
||||
|
||||
@@ -72,17 +72,24 @@ export const decryptAccount = async <
|
||||
account: T,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
|
||||
): Promise<
|
||||
Omit<T, "encryptedCredentials" | "encryptedLastRotationMessage"> & {
|
||||
credentials: TPamAccountCredentials;
|
||||
lastRotationMessage: string | null;
|
||||
}
|
||||
> => {
|
||||
const { encryptedCredentials, encryptedLastRotationMessage, ...rest } = account;
|
||||
|
||||
return {
|
||||
...account,
|
||||
...rest,
|
||||
credentials: await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
encryptedCredentials,
|
||||
projectId,
|
||||
kmsService
|
||||
}),
|
||||
lastRotationMessage: account.encryptedLastRotationMessage
|
||||
lastRotationMessage: encryptedLastRotationMessage
|
||||
? await decryptAccountMessage({
|
||||
encryptedMessage: account.encryptedLastRotationMessage,
|
||||
encryptedMessage: encryptedLastRotationMessage,
|
||||
projectId,
|
||||
kmsService
|
||||
})
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamFolders, TPamResources } from "@app/db/schemas";
|
||||
import {
|
||||
extractAwsAccountIdFromArn,
|
||||
generateConsoleFederationUrl,
|
||||
TAwsIamAccountCredentials
|
||||
} from "@app/ee/services/pam-resource/aws-iam";
|
||||
import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory";
|
||||
import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
@@ -10,12 +17,23 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import {
|
||||
BadRequestError,
|
||||
DatabaseError,
|
||||
ForbiddenRequestError,
|
||||
NotFoundError,
|
||||
PolicyViolationError
|
||||
} from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TApprovalPolicyDALFactory } from "@app/services/approval-policy/approval-policy-dal";
|
||||
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||
import { APPROVAL_POLICY_FACTORY_MAP } from "@app/services/approval-policy/approval-policy-factory";
|
||||
import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TPamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@@ -27,7 +45,8 @@ import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns";
|
||||
import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal";
|
||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
|
||||
import { TSqlResourceConnectionDetails } from "../pam-resource/shared/sql/sql-resource-types";
|
||||
import { TSqlAccountCredentials, TSqlResourceConnectionDetails } from "../pam-resource/shared/sql/sql-resource-types";
|
||||
import { TSSHAccountCredentials } from "../pam-resource/ssh/ssh-resource-types";
|
||||
import { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
|
||||
import { PamSessionStatus } from "../pam-session/pam-session-enums";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
@@ -51,6 +70,9 @@ type TPamAccountServiceFactoryDep = {
|
||||
>;
|
||||
userDAL: TUserDALFactory;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
approvalPolicyDAL: TApprovalPolicyDALFactory;
|
||||
approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory;
|
||||
pamSessionExpirationService: Pick<TPamSessionExpirationServiceFactory, "scheduleSessionExpiration">;
|
||||
};
|
||||
export type TPamAccountServiceFactory = ReturnType<typeof pamAccountServiceFactory>;
|
||||
|
||||
@@ -67,7 +89,10 @@ export const pamAccountServiceFactory = ({
|
||||
licenseService,
|
||||
kmsService,
|
||||
gatewayV2Service,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
approvalPolicyDAL,
|
||||
approvalRequestGrantsDAL,
|
||||
pamSessionExpirationService
|
||||
}: TPamAccountServiceFactoryDep) => {
|
||||
const create = async (
|
||||
{
|
||||
@@ -135,7 +160,8 @@ export const pamAccountServiceFactory = ({
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
resource.projectId
|
||||
);
|
||||
const validatedCredentials = await factory.validateAccountCredentials(credentials);
|
||||
|
||||
@@ -250,7 +276,8 @@ export const pamAccountServiceFactory = ({
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
account.projectId
|
||||
);
|
||||
|
||||
const decryptedCredentials = await decryptAccountCredentials({
|
||||
@@ -279,17 +306,27 @@ export const pamAccountServiceFactory = ({
|
||||
return decryptAccount(account, account.projectId, kmsService);
|
||||
}
|
||||
|
||||
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
|
||||
try {
|
||||
const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc);
|
||||
|
||||
return {
|
||||
...(await decryptAccount(updatedAccount, account.projectId, kmsService)),
|
||||
resource: {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials
|
||||
return {
|
||||
...(await decryptAccount(updatedAccount, account.projectId, kmsService)),
|
||||
resource: {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `Account with name '${name}' already exists for this path`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteById = async (id: string, actor: OrgServiceActor) => {
|
||||
@@ -428,7 +465,7 @@ export const pamAccountServiceFactory = ({
|
||||
const totalCount = totalFolderCount + totalAccountCount;
|
||||
|
||||
const decryptedAndPermittedAccounts: Array<
|
||||
TPamAccounts & {
|
||||
Omit<TPamAccounts, "encryptedCredentials" | "encryptedLastRotationMessage"> & {
|
||||
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
||||
credentials: TPamAccountCredentials;
|
||||
lastRotationMessage: string | null;
|
||||
@@ -487,7 +524,7 @@ export const pamAccountServiceFactory = ({
|
||||
};
|
||||
|
||||
const access = async (
|
||||
{ accountId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
|
||||
{ accountPath, projectId, actorEmail, actorIp, actorName, actorUserAgent, duration }: TAccessAccountDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(actor.orgId);
|
||||
@@ -497,50 +534,83 @@ export const pamAccountServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findById(accountId);
|
||||
if (!account) throw new NotFoundError({ message: `Account with ID '${accountId}' not found` });
|
||||
const pathSegments: string[] = accountPath.split("/").filter(Boolean);
|
||||
if (pathSegments.length === 0) {
|
||||
throw new BadRequestError({ message: "Invalid accountPath. Path must contain at least the account name." });
|
||||
}
|
||||
|
||||
const accountName: string = pathSegments[pathSegments.length - 1] ?? "";
|
||||
const folderPathSegments: string[] = pathSegments.slice(0, -1);
|
||||
|
||||
const folderPath: string = folderPathSegments.length > 0 ? `/${folderPathSegments.join("/")}` : "/";
|
||||
|
||||
let folderId: string | null = null;
|
||||
if (folderPath !== "/") {
|
||||
const folder = await pamFolderDAL.findByPath(projectId, folderPath);
|
||||
if (!folder) {
|
||||
throw new NotFoundError({ message: `Folder at path '${folderPath}' not found` });
|
||||
}
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const account = await pamAccountDAL.findOne({
|
||||
projectId,
|
||||
folderId,
|
||||
name: accountName
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new NotFoundError({
|
||||
message: `Account with name '${accountName}' not found at path '${accountPath}'`
|
||||
});
|
||||
}
|
||||
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
const fac = APPROVAL_POLICY_FACTORY_MAP[ApprovalPolicyType.PamAccess](ApprovalPolicyType.PamAccess);
|
||||
|
||||
const accountPath = await getFullPamFolderPath({
|
||||
pamFolderDAL,
|
||||
folderId: account.folderId,
|
||||
projectId: account.projectId
|
||||
});
|
||||
const inputs = {
|
||||
resourceId: resource.id,
|
||||
accountPath: path.join(folderPath, account.name)
|
||||
};
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Access,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
})
|
||||
);
|
||||
const canAccess = await fac.canAccess(approvalRequestGrantsDAL, resource.projectId, actor.id, inputs);
|
||||
|
||||
const session = await pamSessionDAL.create({
|
||||
accountName: account.name,
|
||||
actorEmail,
|
||||
actorIp,
|
||||
actorName,
|
||||
actorUserAgent,
|
||||
projectId: account.projectId,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
status: PamSessionStatus.Starting,
|
||||
accountId: account.id,
|
||||
userId: actor.id,
|
||||
expiresAt: new Date(Date.now() + duration)
|
||||
});
|
||||
// Grant does not exist, check policy and fallback to permission check
|
||||
if (!canAccess) {
|
||||
const policy = await fac.matchPolicy(approvalPolicyDAL, resource.projectId, inputs);
|
||||
|
||||
if (policy) {
|
||||
throw new PolicyViolationError({
|
||||
message: "A policy is in place for this resource",
|
||||
details: {
|
||||
policyId: policy.id,
|
||||
policyName: policy.name,
|
||||
policyType: policy.type
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If there isn't a policy in place, continue with checking permission
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorId: actor.id,
|
||||
actorOrgId: actor.orgId,
|
||||
projectId: account.projectId,
|
||||
actionProjectType: ActionProjectType.PAM
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPamAccountActions.Access,
|
||||
subject(ProjectPermissionSub.PamAccounts, {
|
||||
resourceName: resource.name,
|
||||
accountName: account.name,
|
||||
accountPath: folderPath
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { connectionDetails, gatewayId, resourceType } = await decryptResource(
|
||||
resource,
|
||||
@@ -551,13 +621,81 @@ export const pamAccountServiceFactory = ({
|
||||
const user = await userDAL.findById(actor.id);
|
||||
if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` });
|
||||
|
||||
if (resourceType === PamResource.AwsIam) {
|
||||
const awsCredentials = (await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: account.projectId
|
||||
})) as TAwsIamAccountCredentials;
|
||||
|
||||
const { consoleUrl, expiresAt } = await generateConsoleFederationUrl({
|
||||
connectionDetails,
|
||||
targetRoleArn: awsCredentials.targetRoleArn,
|
||||
roleSessionName: actorEmail,
|
||||
projectId: account.projectId, // Use project ID as External ID for security
|
||||
sessionDuration: awsCredentials.defaultSessionDuration
|
||||
});
|
||||
|
||||
const session = await pamSessionDAL.create({
|
||||
accountName: account.name,
|
||||
actorEmail,
|
||||
actorIp,
|
||||
actorName,
|
||||
actorUserAgent,
|
||||
projectId: account.projectId,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
status: PamSessionStatus.Active, // AWS IAM sessions are immediately active
|
||||
accountId: account.id,
|
||||
userId: actor.id,
|
||||
expiresAt,
|
||||
startedAt: new Date()
|
||||
});
|
||||
|
||||
// Schedule session expiration job to run at expiresAt
|
||||
await pamSessionExpirationService.scheduleSessionExpiration(session.id, expiresAt);
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
resourceType,
|
||||
account,
|
||||
consoleUrl,
|
||||
metadata: {
|
||||
awsAccountId: extractAwsAccountIdFromArn(connectionDetails.roleArn),
|
||||
targetRoleArn: awsCredentials.targetRoleArn,
|
||||
federatedUsername: actorEmail,
|
||||
expiresAt: expiresAt.toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// For gateway-based resources (Postgres, MySQL, SSH), create session first
|
||||
const session = await pamSessionDAL.create({
|
||||
accountName: account.name,
|
||||
actorEmail,
|
||||
actorIp,
|
||||
actorName,
|
||||
actorUserAgent,
|
||||
projectId,
|
||||
resourceName: resource.name,
|
||||
resourceType: resource.resourceType,
|
||||
status: PamSessionStatus.Starting,
|
||||
accountId: account.id,
|
||||
userId: actor.id,
|
||||
expiresAt: new Date(Date.now() + duration)
|
||||
});
|
||||
|
||||
if (!gatewayId) {
|
||||
throw new BadRequestError({ message: "Gateway ID is required for this resource type" });
|
||||
}
|
||||
|
||||
const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({
|
||||
gatewayId,
|
||||
duration,
|
||||
sessionId: session.id,
|
||||
resourceType: resource.resourceType as PamResource,
|
||||
host: connectionDetails.host,
|
||||
port: connectionDetails.port,
|
||||
host: (connectionDetails as TSqlResourceConnectionDetails).host,
|
||||
port: (connectionDetails as TSqlResourceConnectionDetails).port,
|
||||
actorMetadata: {
|
||||
id: actor.id,
|
||||
type: actor.type,
|
||||
@@ -578,30 +716,30 @@ export const pamAccountServiceFactory = ({
|
||||
const connectionCredentials = (await decryptResourceConnectionDetails({
|
||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||
kmsService,
|
||||
projectId: account.projectId
|
||||
projectId
|
||||
})) as TSqlResourceConnectionDetails;
|
||||
|
||||
const credentials = await decryptAccountCredentials({
|
||||
const credentials = (await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: account.projectId
|
||||
});
|
||||
projectId
|
||||
})) as TSqlAccountCredentials;
|
||||
|
||||
metadata = {
|
||||
username: credentials.username,
|
||||
database: connectionCredentials.database,
|
||||
accountName: account.name,
|
||||
accountPath
|
||||
accountPath: folderPath
|
||||
};
|
||||
}
|
||||
break;
|
||||
case PamResource.SSH:
|
||||
{
|
||||
const credentials = await decryptAccountCredentials({
|
||||
const credentials = (await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: account.projectId
|
||||
});
|
||||
projectId
|
||||
})) as TSSHAccountCredentials;
|
||||
|
||||
metadata = {
|
||||
username: credentials.username
|
||||
@@ -622,7 +760,7 @@ export const pamAccountServiceFactory = ({
|
||||
gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey,
|
||||
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
|
||||
relayHost: gatewayConnectionDetails.relayHost,
|
||||
projectId: account.projectId,
|
||||
projectId,
|
||||
account,
|
||||
metadata
|
||||
};
|
||||
@@ -674,7 +812,7 @@ export const pamAccountServiceFactory = ({
|
||||
const resource = await pamResourceDAL.findById(account.resourceId);
|
||||
if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` });
|
||||
|
||||
if (resource.gatewayIdentityId !== actor.id) {
|
||||
if (resource.gatewayId && resource.gatewayIdentityId !== actor.id) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Identity does not have access to fetch the PAM session credentials"
|
||||
});
|
||||
@@ -738,7 +876,8 @@ export const pamAccountServiceFactory = ({
|
||||
resourceType as PamResource,
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
account.projectId
|
||||
);
|
||||
|
||||
const newCredentials = await factory.rotateAccountCredentials(
|
||||
|
||||
@@ -6,15 +6,18 @@ import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums";
|
||||
// DTOs
|
||||
export type TCreateAccountDTO = Pick<
|
||||
TPamAccount,
|
||||
"name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationEnabled" | "rotationIntervalSeconds"
|
||||
>;
|
||||
"name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationIntervalSeconds"
|
||||
> & {
|
||||
rotationEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateAccountDTO = Partial<Omit<TCreateAccountDTO, "folderId" | "resourceId">> & {
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
export type TAccessAccountDTO = {
|
||||
accountId: string;
|
||||
accountPath: string;
|
||||
projectId: string;
|
||||
actorEmail: string;
|
||||
actorIp: string;
|
||||
actorName: string;
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { AssumeRoleCommand, Credentials, STSClient, STSClientConfig } from "@aws-sdk/client-sts";
|
||||
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
|
||||
import { TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types";
|
||||
|
||||
const AWS_STS_MIN_DURATION_SECONDS = 900;
|
||||
|
||||
// We hardcode us-east-1 because:
|
||||
// 1. IAM is global - roles can be assumed from any STS regional endpoint
|
||||
// 2. The temporary credentials returned work globally across all AWS regions
|
||||
// 3. The target account's resources can be in any region - it doesn't affect STS calls
|
||||
const AWS_STS_DEFAULT_REGION = "us-east-1";
|
||||
|
||||
const createStsClient = (credentials?: Credentials): STSClient => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const config: STSClientConfig = {
|
||||
region: AWS_STS_DEFAULT_REGION,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher
|
||||
};
|
||||
|
||||
if (credentials) {
|
||||
// Use provided credentials (for role chaining)
|
||||
config.credentials = {
|
||||
accessKeyId: credentials.AccessKeyId!,
|
||||
secretAccessKey: credentials.SecretAccessKey!,
|
||||
sessionToken: credentials.SessionToken
|
||||
};
|
||||
} else if (appCfg.PAM_AWS_ACCESS_KEY_ID && appCfg.PAM_AWS_SECRET_ACCESS_KEY) {
|
||||
// Use configured static credentials
|
||||
config.credentials = {
|
||||
accessKeyId: appCfg.PAM_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.PAM_AWS_SECRET_ACCESS_KEY
|
||||
};
|
||||
}
|
||||
// Otherwise uses instance profile if hosting on AWS
|
||||
|
||||
return new STSClient(config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assumes the PAM role and returns the credentials.
|
||||
* Returns null if assumption fails (for validation) or throws if throwOnError is true.
|
||||
*/
|
||||
const assumePamRole = async ({
|
||||
connectionDetails,
|
||||
projectId,
|
||||
sessionDuration = AWS_STS_MIN_DURATION_SECONDS,
|
||||
sessionNameSuffix = "validation",
|
||||
throwOnError = false
|
||||
}: {
|
||||
connectionDetails: TAwsIamResourceConnectionDetails;
|
||||
projectId: string;
|
||||
sessionDuration?: number;
|
||||
sessionNameSuffix?: string;
|
||||
throwOnError?: boolean;
|
||||
}): Promise<Credentials | null> => {
|
||||
const stsClient = createStsClient();
|
||||
|
||||
try {
|
||||
const result = await stsClient.send(
|
||||
new AssumeRoleCommand({
|
||||
RoleArn: connectionDetails.roleArn,
|
||||
RoleSessionName: `infisical-pam-${sessionNameSuffix}-${Date.now()}`,
|
||||
DurationSeconds: sessionDuration,
|
||||
ExternalId: projectId
|
||||
})
|
||||
);
|
||||
|
||||
if (!result.Credentials) {
|
||||
if (throwOnError) {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to assume PAM role - AWS STS did not return credentials"
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.Credentials;
|
||||
} catch (error) {
|
||||
if (throwOnError) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to assume PAM role - AWS STS did not return credentials: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assumes a target role using PAM role credentials (role chaining).
|
||||
* Returns null if assumption fails (for validation) or throws if throwOnError is true.
|
||||
*/
|
||||
const assumeTargetRole = async ({
|
||||
pamCredentials,
|
||||
targetRoleArn,
|
||||
projectId,
|
||||
roleSessionName,
|
||||
sessionDuration = AWS_STS_MIN_DURATION_SECONDS,
|
||||
throwOnError = false
|
||||
}: {
|
||||
pamCredentials: Credentials;
|
||||
targetRoleArn: string;
|
||||
projectId: string;
|
||||
roleSessionName: string;
|
||||
sessionDuration?: number;
|
||||
throwOnError?: boolean;
|
||||
}): Promise<Credentials | null> => {
|
||||
const chainedStsClient = createStsClient(pamCredentials);
|
||||
|
||||
try {
|
||||
const result = await chainedStsClient.send(
|
||||
new AssumeRoleCommand({
|
||||
RoleArn: targetRoleArn,
|
||||
RoleSessionName: roleSessionName,
|
||||
DurationSeconds: sessionDuration,
|
||||
ExternalId: projectId
|
||||
})
|
||||
);
|
||||
|
||||
if (!result.Credentials) {
|
||||
if (throwOnError) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to assume target role - verify the target role trust policy allows the PAM role to assume it"
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.Credentials;
|
||||
} catch (error) {
|
||||
if (throwOnError) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to assume target role - AWS STS did not return credentials: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const validatePamRoleConnection = async (
|
||||
connectionDetails: TAwsIamResourceConnectionDetails,
|
||||
projectId: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const credentials = await assumePamRole({ connectionDetails, projectId });
|
||||
return credentials !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const validateTargetRoleAssumption = async ({
|
||||
connectionDetails,
|
||||
targetRoleArn,
|
||||
projectId
|
||||
}: {
|
||||
connectionDetails: TAwsIamResourceConnectionDetails;
|
||||
targetRoleArn: string;
|
||||
projectId: string;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
const pamCredentials = await assumePamRole({ connectionDetails, projectId });
|
||||
if (!pamCredentials) return false;
|
||||
|
||||
const targetCredentials = await assumeTargetRole({
|
||||
pamCredentials,
|
||||
targetRoleArn,
|
||||
projectId,
|
||||
roleSessionName: `infisical-pam-target-validation-${Date.now()}`
|
||||
});
|
||||
return targetCredentials !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assumes the target role and generates a federated console sign-in URL.
|
||||
*/
|
||||
export const generateConsoleFederationUrl = async ({
|
||||
connectionDetails,
|
||||
targetRoleArn,
|
||||
roleSessionName,
|
||||
projectId,
|
||||
sessionDuration
|
||||
}: {
|
||||
connectionDetails: TAwsIamResourceConnectionDetails;
|
||||
targetRoleArn: string;
|
||||
roleSessionName: string;
|
||||
projectId: string;
|
||||
sessionDuration: number;
|
||||
}): Promise<{ consoleUrl: string; expiresAt: Date }> => {
|
||||
const pamCredentials = await assumePamRole({
|
||||
connectionDetails,
|
||||
projectId,
|
||||
sessionDuration,
|
||||
sessionNameSuffix: "session",
|
||||
throwOnError: true
|
||||
});
|
||||
|
||||
const targetCredentials = await assumeTargetRole({
|
||||
pamCredentials: pamCredentials!,
|
||||
targetRoleArn,
|
||||
projectId,
|
||||
roleSessionName,
|
||||
sessionDuration,
|
||||
throwOnError: true
|
||||
});
|
||||
|
||||
const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = targetCredentials!;
|
||||
|
||||
// Generate federation URL
|
||||
const sessionJson = JSON.stringify({
|
||||
sessionId: AccessKeyId,
|
||||
sessionKey: SecretAccessKey,
|
||||
sessionToken: SessionToken
|
||||
});
|
||||
|
||||
const federationEndpoint = "https://signin.aws.amazon.com/federation";
|
||||
|
||||
const signinTokenUrl = `${federationEndpoint}?Action=getSigninToken&Session=${encodeURIComponent(sessionJson)}`;
|
||||
|
||||
const tokenResponse = await request.get<{ SigninToken?: string }>(signinTokenUrl);
|
||||
|
||||
if (!tokenResponse.data.SigninToken) {
|
||||
throw new InternalServerError({
|
||||
message: `AWS federation endpoint did not return a SigninToken: ${JSON.stringify(tokenResponse.data).substring(0, 200)}`
|
||||
});
|
||||
}
|
||||
|
||||
const consoleDestination = `https://console.aws.amazon.com/`;
|
||||
const consoleUrl = `${federationEndpoint}?Action=login&SigninToken=${encodeURIComponent(tokenResponse.data.SigninToken)}&Destination=${encodeURIComponent(consoleDestination)}`;
|
||||
|
||||
return {
|
||||
consoleUrl,
|
||||
expiresAt: Expiration ?? new Date(Date.now() + sessionDuration * 1000)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
TPamResourceFactory,
|
||||
TPamResourceFactoryRotateAccountCredentials,
|
||||
TPamResourceFactoryValidateAccountCredentials
|
||||
} from "../pam-resource-types";
|
||||
import { validatePamRoleConnection, validateTargetRoleAssumption } from "./aws-iam-federation";
|
||||
import { TAwsIamAccountCredentials, TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types";
|
||||
|
||||
export const awsIamResourceFactory: TPamResourceFactory<TAwsIamResourceConnectionDetails, TAwsIamAccountCredentials> = (
|
||||
resourceType: PamResource,
|
||||
connectionDetails: TAwsIamResourceConnectionDetails,
|
||||
// AWS IAM doesn't use gateway
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_gatewayId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_gatewayV2Service,
|
||||
projectId
|
||||
) => {
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
const isValid = await validatePamRoleConnection(connectionDetails, projectId ?? "");
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Unable to assume the PAM role. Verify the role ARN and ensure the trust policy allows Infisical to assume the role."
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ roleArn: connectionDetails.roleArn },
|
||||
"[AWS IAM Resource Factory] PAM role connection validated successfully"
|
||||
);
|
||||
|
||||
return connectionDetails;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.error(error, "[AWS IAM Resource Factory] Failed to validate PAM role connection");
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<TAwsIamAccountCredentials> = async (
|
||||
credentials
|
||||
) => {
|
||||
try {
|
||||
const isValid = await validateTargetRoleAssumption({
|
||||
connectionDetails,
|
||||
targetRoleArn: credentials.targetRoleArn,
|
||||
projectId: projectId ?? ""
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to assume the target role. Verify the target role ARN and ensure the PAM role (ARN: ${connectionDetails.roleArn}) has permission to assume it.`
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ targetRoleArn: credentials.targetRoleArn },
|
||||
"[AWS IAM Resource Factory] Target role credentials validated successfully"
|
||||
);
|
||||
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.error(error, "[AWS IAM Resource Factory] Failed to validate target role credentials");
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<TAwsIamAccountCredentials> = async (
|
||||
_rotationAccountCredentials,
|
||||
currentCredentials
|
||||
) => {
|
||||
return currentCredentials;
|
||||
};
|
||||
|
||||
const handleOverwritePreventionForCensoredValues = async (
|
||||
updatedAccountCredentials: TAwsIamAccountCredentials,
|
||||
// AWS IAM has no censored credential values - role ARNs are not secrets
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_currentCredentials: TAwsIamAccountCredentials
|
||||
) => {
|
||||
return updatedAccountCredentials;
|
||||
};
|
||||
|
||||
return {
|
||||
validateConnection,
|
||||
validateAccountCredentials,
|
||||
rotateAccountCredentials,
|
||||
handleOverwritePreventionForCensoredValues
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { AwsIamResourceListItemSchema } from "./aws-iam-resource-schemas";
|
||||
|
||||
export const getAwsIamResourceListItem = () => {
|
||||
return {
|
||||
name: AwsIamResourceListItemSchema.shape.name.value,
|
||||
resource: AwsIamResourceListItemSchema.shape.resource.value
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the AWS Account ID from an IAM Role ARN
|
||||
* ARN format: arn:aws:iam::123456789012:role/RoleName
|
||||
*/
|
||||
export const extractAwsAccountIdFromArn = (roleArn: string): string => {
|
||||
const match = roleArn.match(new RE2("^arn:aws:iam::(\\d{12}):role/"));
|
||||
if (!match) {
|
||||
throw new BadRequestError({ message: "Invalid IAM Role ARN format" });
|
||||
}
|
||||
return match[1];
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
} from "../pam-resource-schemas";
|
||||
|
||||
// AWS STS session duration limits (in seconds)
|
||||
// Role chaining (Infisical → PAM role → target role) limits max session to 1 hour
|
||||
// @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
const AWS_STS_MIN_SESSION_DURATION = 900; // 15 minutes
|
||||
const AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING = 3600; // 1 hour
|
||||
|
||||
export const AwsIamResourceConnectionDetailsSchema = z.object({
|
||||
roleArn: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export const AwsIamAccountCredentialsSchema = z.object({
|
||||
targetRoleArn: z.string().trim().min(1).max(2048),
|
||||
defaultSessionDuration: z.coerce
|
||||
.number()
|
||||
.min(AWS_STS_MIN_SESSION_DURATION)
|
||||
.max(AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING)
|
||||
});
|
||||
|
||||
const BaseAwsIamResourceSchema = BasePamResourceSchema.extend({
|
||||
resourceType: z.literal(PamResource.AwsIam),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export const AwsIamResourceSchema = BaseAwsIamResourceSchema.extend({
|
||||
connectionDetails: AwsIamResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const SanitizedAwsIamResourceSchema = BaseAwsIamResourceSchema.extend({
|
||||
connectionDetails: AwsIamResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const AwsIamResourceListItemSchema = z.object({
|
||||
name: z.literal("AWS IAM"),
|
||||
resource: z.literal(PamResource.AwsIam)
|
||||
});
|
||||
|
||||
export const CreateAwsIamResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
connectionDetails: AwsIamResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdateAwsIamResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
connectionDetails: AwsIamResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const AwsIamAccountSchema = BasePamAccountSchema.extend({
|
||||
credentials: AwsIamAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreateAwsIamAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||
credentials: AwsIamAccountCredentialsSchema,
|
||||
// AWS IAM accounts don't support credential rotation - they use role assumption
|
||||
rotationEnabled: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export const UpdateAwsIamAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||
credentials: AwsIamAccountCredentialsSchema.optional()
|
||||
});
|
||||
|
||||
export const SanitizedAwsIamAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||
credentials: AwsIamAccountCredentialsSchema.pick({
|
||||
targetRoleArn: true,
|
||||
defaultSessionDuration: true
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AwsIamAccountCredentialsSchema,
|
||||
AwsIamAccountSchema,
|
||||
AwsIamResourceConnectionDetailsSchema,
|
||||
AwsIamResourceSchema
|
||||
} from "./aws-iam-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export type TAwsIamResource = z.infer<typeof AwsIamResourceSchema>;
|
||||
export type TAwsIamResourceConnectionDetails = z.infer<typeof AwsIamResourceConnectionDetailsSchema>;
|
||||
|
||||
// Accounts
|
||||
export type TAwsIamAccount = z.infer<typeof AwsIamAccountSchema>;
|
||||
export type TAwsIamAccountCredentials = z.infer<typeof AwsIamAccountCredentialsSchema>;
|
||||
5
backend/src/ee/services/pam-resource/aws-iam/index.ts
Normal file
5
backend/src/ee/services/pam-resource/aws-iam/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./aws-iam-federation";
|
||||
export * from "./aws-iam-resource-factory";
|
||||
export * from "./aws-iam-resource-fns";
|
||||
export * from "./aws-iam-resource-schemas";
|
||||
export * from "./aws-iam-resource-types";
|
||||
@@ -2,13 +2,13 @@ import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreateGatewayPamResourceSchema,
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
BaseUpdateGatewayPamResourceSchema,
|
||||
BaseUpdatePamAccountSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import {
|
||||
BaseSqlAccountCredentialsSchema,
|
||||
@@ -43,12 +43,12 @@ export const MySQLResourceListItemSchema = z.object({
|
||||
resource: z.literal(PamResource.MySQL)
|
||||
});
|
||||
|
||||
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
export const CreateMySQLResourceSchema = BaseCreateGatewayPamResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
export const UpdateMySQLResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export const pamResourceDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
const doc = await (tx || db.replicaNode())(TableName.PamResource)
|
||||
.join(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`)
|
||||
.leftJoin(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`)
|
||||
.select(selectAllTableCols(TableName.PamResource))
|
||||
.select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName"))
|
||||
.select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId"))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export enum PamResource {
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql",
|
||||
SSH = "ssh"
|
||||
SSH = "ssh",
|
||||
AwsIam = "aws-iam"
|
||||
}
|
||||
|
||||
export enum PamResourceOrderBy {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { awsIamResourceFactory } from "./aws-iam/aws-iam-resource-factory";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types";
|
||||
import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
||||
@@ -8,5 +9,6 @@ type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnect
|
||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation
|
||||
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.AwsIam]: awsIamResourceFactory as TPamResourceFactoryImplementation
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||
import { getAwsIamResourceListItem } from "./aws-iam/aws-iam-resource-fns";
|
||||
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||
|
||||
export const listResourceOptions = () => {
|
||||
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [getPostgresResourceListItem(), getMySQLResourceListItem(), getAwsIamResourceListItem()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
};
|
||||
|
||||
// Resource
|
||||
|
||||
@@ -3,6 +3,18 @@ import { z } from "zod";
|
||||
import { PamAccountsSchema, PamResourcesSchema } from "@app/db/schemas";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
export const GatewayAccessResponseSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
relayClientCertificate: z.string(),
|
||||
relayClientPrivateKey: z.string(),
|
||||
relayServerCertificateChain: z.string(),
|
||||
gatewayClientCertificate: z.string(),
|
||||
gatewayClientPrivateKey: z.string(),
|
||||
gatewayServerCertificateChain: z.string(),
|
||||
relayHost: z.string(),
|
||||
metadata: z.record(z.string(), z.string().optional()).optional()
|
||||
});
|
||||
|
||||
// Resources
|
||||
export const BasePamResourceSchema = PamResourcesSchema.omit({
|
||||
encryptedConnectionDetails: true,
|
||||
@@ -10,17 +22,27 @@ export const BasePamResourceSchema = PamResourcesSchema.omit({
|
||||
resourceType: true
|
||||
});
|
||||
|
||||
export const BaseCreatePamResourceSchema = z.object({
|
||||
const CoreCreatePamResourceSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
gatewayId: z.string().uuid(),
|
||||
name: slugSchema({ field: "name" })
|
||||
});
|
||||
|
||||
export const BaseUpdatePamResourceSchema = z.object({
|
||||
gatewayId: z.string().uuid().optional(),
|
||||
export const BaseCreateGatewayPamResourceSchema = CoreCreatePamResourceSchema.extend({
|
||||
gatewayId: z.string().uuid()
|
||||
});
|
||||
|
||||
export const BaseCreatePamResourceSchema = CoreCreatePamResourceSchema;
|
||||
|
||||
const CoreUpdatePamResourceSchema = z.object({
|
||||
name: slugSchema({ field: "name" }).optional()
|
||||
});
|
||||
|
||||
export const BaseUpdateGatewayPamResourceSchema = CoreUpdatePamResourceSchema.extend({
|
||||
gatewayId: z.string().uuid().optional()
|
||||
});
|
||||
|
||||
export const BaseUpdatePamResourceSchema = CoreUpdatePamResourceSchema;
|
||||
|
||||
// Accounts
|
||||
export const BasePamAccountSchema = PamAccountsSchema.omit({
|
||||
encryptedCredentials: true
|
||||
|
||||
@@ -92,7 +92,8 @@ export const pamResourceServiceFactory = ({
|
||||
resourceType,
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
projectId
|
||||
);
|
||||
|
||||
const validatedConnectionDetails = await factory.validateConnection();
|
||||
@@ -162,7 +163,8 @@ export const pamResourceServiceFactory = ({
|
||||
resource.resourceType as PamResource,
|
||||
connectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
resource.projectId
|
||||
);
|
||||
const validatedConnectionDetails = await factory.validateConnection();
|
||||
const encryptedConnectionDetails = await encryptResourceConnectionDetails({
|
||||
@@ -189,7 +191,8 @@ export const pamResourceServiceFactory = ({
|
||||
resource.resourceType as PamResource,
|
||||
decryptedConnectionDetails,
|
||||
resource.gatewayId,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
resource.projectId
|
||||
);
|
||||
|
||||
let finalCredentials = { ...rotationAccountCredentials };
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import {
|
||||
TAwsIamAccount,
|
||||
TAwsIamAccountCredentials,
|
||||
TAwsIamResource,
|
||||
TAwsIamResourceConnectionDetails
|
||||
} from "./aws-iam/aws-iam-resource-types";
|
||||
import {
|
||||
TMySQLAccount,
|
||||
TMySQLAccountCredentials,
|
||||
@@ -22,22 +28,28 @@ import {
|
||||
} from "./ssh/ssh-resource-types";
|
||||
|
||||
// Resource types
|
||||
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource;
|
||||
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TAwsIamResource;
|
||||
export type TPamResourceConnectionDetails =
|
||||
| TPostgresResourceConnectionDetails
|
||||
| TMySQLResourceConnectionDetails
|
||||
| TSSHResourceConnectionDetails;
|
||||
| TSSHResourceConnectionDetails
|
||||
| TAwsIamResourceConnectionDetails;
|
||||
|
||||
// Account types
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount;
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials | TSSHAccountCredentials;
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TAwsIamAccount;
|
||||
|
||||
export type TPamAccountCredentials =
|
||||
| TPostgresAccountCredentials
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
| TMySQLAccountCredentials
|
||||
| TSSHAccountCredentials
|
||||
| TAwsIamAccountCredentials;
|
||||
|
||||
// Resource DTOs
|
||||
export type TCreateResourceDTO = Pick<
|
||||
TPamResource,
|
||||
"name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" | "rotationAccountCredentials"
|
||||
>;
|
||||
export type TCreateResourceDTO = Pick<TPamResource, "name" | "connectionDetails" | "resourceType" | "projectId"> & {
|
||||
gatewayId?: string | null;
|
||||
rotationAccountCredentials?: TPamAccountCredentials | null;
|
||||
};
|
||||
|
||||
export type TUpdateResourceDTO = Partial<Omit<TCreateResourceDTO, "resourceType" | "projectId">> & {
|
||||
resourceId: string;
|
||||
@@ -65,8 +77,9 @@ export type TPamResourceFactoryRotateAccountCredentials<C extends TPamAccountCre
|
||||
export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C extends TPamAccountCredentials> = (
|
||||
resourceType: PamResource,
|
||||
connectionDetails: T,
|
||||
gatewayId: string,
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
||||
gatewayId: string | null | undefined,
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||
projectId: string | null | undefined
|
||||
) => {
|
||||
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
||||
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
||||
|
||||
@@ -2,13 +2,13 @@ import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreateGatewayPamResourceSchema,
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
BaseUpdateGatewayPamResourceSchema,
|
||||
BaseUpdatePamAccountSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import {
|
||||
BaseSqlAccountCredentialsSchema,
|
||||
@@ -40,12 +40,12 @@ export const PostgresResourceListItemSchema = z.object({
|
||||
resource: z.literal(PamResource.Postgres)
|
||||
});
|
||||
|
||||
export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
export const CreatePostgresResourceSchema = BaseCreateGatewayPamResourceSchema.extend({
|
||||
connectionDetails: PostgresResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
export const UpdatePostgresResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({
|
||||
connectionDetails: PostgresResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
@@ -233,6 +233,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
gatewayV2Service
|
||||
) => {
|
||||
const validateConnection = async () => {
|
||||
if (!gatewayId) {
|
||||
throw new BadRequestError({ message: "Gateway ID is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||
await client.validate(true);
|
||||
@@ -255,6 +259,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
credentials
|
||||
) => {
|
||||
try {
|
||||
if (!gatewayId) {
|
||||
throw new BadRequestError({ message: "Gateway ID is required" });
|
||||
}
|
||||
|
||||
await executeWithGateway(
|
||||
{
|
||||
connectionDetails,
|
||||
@@ -296,6 +304,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
currentCredentials
|
||||
) => {
|
||||
const newPassword = alphaNumericNanoId(32);
|
||||
if (!gatewayId) {
|
||||
throw new BadRequestError({ message: "Gateway ID is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
return await executeWithGateway(
|
||||
{
|
||||
|
||||
@@ -60,6 +60,10 @@ export const sshResourceFactory: TPamResourceFactory<TSSHResourceConnectionDetai
|
||||
) => {
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
if (!gatewayId) {
|
||||
throw new BadRequestError({ message: "Gateway ID is required" });
|
||||
}
|
||||
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const client = new Client();
|
||||
@@ -131,6 +135,10 @@ export const sshResourceFactory: TPamResourceFactory<TSSHResourceConnectionDetai
|
||||
credentials
|
||||
) => {
|
||||
try {
|
||||
if (!gatewayId) {
|
||||
throw new BadRequestError({ message: "Gateway ID is required" });
|
||||
}
|
||||
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const client = new Client();
|
||||
|
||||
@@ -2,13 +2,13 @@ import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreateGatewayPamResourceSchema,
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
BaseUpdateGatewayPamResourceSchema,
|
||||
BaseUpdatePamAccountSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import { SSHAuthMethod } from "./ssh-resource-enums";
|
||||
|
||||
@@ -73,12 +73,12 @@ export const SanitizedSSHResourceSchema = BaseSSHResourceSchema.extend({
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const CreateSSHResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
export const CreateSSHResourceSchema = BaseCreateGatewayPamResourceSchema.extend({
|
||||
connectionDetails: SSHResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdateSSHResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
export const UpdateSSHResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({
|
||||
connectionDetails: SSHResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
import { PamSessionStatus } from "./pam-session-enums";
|
||||
|
||||
export type TPamSessionDALFactory = ReturnType<typeof pamSessionDALFactory>;
|
||||
export const pamSessionDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.PamSession);
|
||||
@@ -22,5 +24,19 @@ export const pamSessionDALFactory = (db: TDbClient) => {
|
||||
return session;
|
||||
};
|
||||
|
||||
return { ...orm, findById };
|
||||
const expireSessionById = async (sessionId: string, tx?: Knex) => {
|
||||
const now = new Date();
|
||||
|
||||
const updatedCount = await (tx || db)(TableName.PamSession)
|
||||
.where("id", sessionId)
|
||||
.whereIn("status", [PamSessionStatus.Active, PamSessionStatus.Starting])
|
||||
.update({
|
||||
status: PamSessionStatus.Ended,
|
||||
endedAt: now
|
||||
});
|
||||
|
||||
return updatedCount;
|
||||
};
|
||||
|
||||
return { ...orm, findById, expireSessionById };
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum PamSessionStatus {
|
||||
Starting = "starting", // Starting, user connecting to resource
|
||||
Active = "active", // Active, user is connected to resource
|
||||
Ended = "ended", // Ended by user
|
||||
Ended = "ended", // Ended by user or automatically expired after expiresAt timestamp
|
||||
Terminated = "terminated" // Terminated by an admin
|
||||
}
|
||||
|
||||
@@ -34,9 +34,40 @@ export const pamSessionServiceFactory = ({
|
||||
licenseService,
|
||||
kmsService
|
||||
}: TPamSessionServiceFactoryDep) => {
|
||||
// Helper to check and update expired sessions when viewing session details (redundancy for scheduled job)
|
||||
// Only applies to non-gateway sessions (e.g., AWS IAM) - gateway sessions are managed by the gateway
|
||||
// This is intentionally only called in getById (session details view), not in list
|
||||
const checkAndExpireSessionIfNeeded = async <
|
||||
T extends { id: string; status: string; expiresAt: Date | null; gatewayIdentityId?: string | null }
|
||||
>(
|
||||
session: T
|
||||
): Promise<T> => {
|
||||
// Skip gateway-based sessions - they have their own lifecycle managed by the gateway
|
||||
if (session.gatewayIdentityId) {
|
||||
return session;
|
||||
}
|
||||
|
||||
const isActive = session.status === PamSessionStatus.Active || session.status === PamSessionStatus.Starting;
|
||||
const isExpired = session.expiresAt && new Date(session.expiresAt) <= new Date();
|
||||
|
||||
if (isActive && isExpired) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const updatedSession = await pamSessionDAL.updateById(session.id, {
|
||||
status: PamSessionStatus.Ended,
|
||||
endedAt: new Date()
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
return { ...session, ...updatedSession };
|
||||
}
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
const getById = async (sessionId: string, actor: OrgServiceActor) => {
|
||||
const session = await pamSessionDAL.findById(sessionId);
|
||||
if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
const sessionFromDb = await pamSessionDAL.findById(sessionId);
|
||||
if (!sessionFromDb) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` });
|
||||
|
||||
const session = await checkAndExpireSessionIfNeeded(sessionFromDb);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
@@ -116,7 +147,7 @@ export const pamSessionServiceFactory = ({
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
if (session.gatewayIdentityId !== actor.id) {
|
||||
if (session.gatewayIdentityId && session.gatewayIdentityId !== actor.id) {
|
||||
throw new ForbiddenRequestError({ message: "Identity does not have access to update logs for this session" });
|
||||
}
|
||||
|
||||
@@ -158,7 +189,7 @@ export const pamSessionServiceFactory = ({
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
if (session.gatewayIdentityId !== actor.id) {
|
||||
if (session.gatewayIdentityId && session.gatewayIdentityId !== actor.id) {
|
||||
throw new ForbiddenRequestError({ message: "Identity does not have access to end this session" });
|
||||
}
|
||||
} else if (actor.type === ActorType.USER) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionAppConnectionActions,
|
||||
ProjectPermissionApprovalRequestActions,
|
||||
ProjectPermissionApprovalRequestGrantActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateAuthorityActions,
|
||||
@@ -339,6 +341,16 @@ const buildAdminPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions);
|
||||
|
||||
can(
|
||||
[ProjectPermissionApprovalRequestActions.Read, ProjectPermissionApprovalRequestActions.Create],
|
||||
ProjectPermissionSub.ApprovalRequests
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionApprovalRequestGrantActions.Read, ProjectPermissionApprovalRequestGrantActions.Revoke],
|
||||
ProjectPermissionSub.ApprovalRequestGrants
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -586,6 +598,8 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionSub.PamAccounts
|
||||
);
|
||||
|
||||
can([ProjectPermissionApprovalRequestActions.Create], ProjectPermissionSub.ApprovalRequests);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
||||
@@ -224,6 +224,16 @@ export enum ProjectPermissionPamSessionActions {
|
||||
// Terminate = "terminate"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionApprovalRequestActions {
|
||||
Read = "read",
|
||||
Create = "create"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionApprovalRequestGrantActions {
|
||||
Read = "read",
|
||||
Revoke = "revoke"
|
||||
}
|
||||
|
||||
export const isCustomProjectRole = (slug: string) =>
|
||||
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||
|
||||
@@ -274,7 +284,9 @@ export enum ProjectPermissionSub {
|
||||
PamResources = "pam-resources",
|
||||
PamAccounts = "pam-accounts",
|
||||
PamSessions = "pam-sessions",
|
||||
CertificateProfiles = "certificate-profiles"
|
||||
CertificateProfiles = "certificate-profiles",
|
||||
ApprovalRequests = "approval-requests",
|
||||
ApprovalRequestGrants = "approval-request-grants"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -500,7 +512,9 @@ export type ProjectPermissionSet =
|
||||
| ProjectPermissionSub.CertificateProfiles
|
||||
| (ForcedSubject<ProjectPermissionSub.CertificateProfiles> & CertificateProfileSubjectFields)
|
||||
)
|
||||
];
|
||||
]
|
||||
| [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests]
|
||||
| [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants];
|
||||
|
||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@@ -1105,6 +1119,18 @@ const GeneralPermissionSchema = [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.ApprovalRequests).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.ApprovalRequestGrants).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestGrantActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./mongodb-credentials-rotation-constants";
|
||||
export * from "./mongodb-credentials-rotation-fns";
|
||||
export * from "./mongodb-credentials-rotation-schemas";
|
||||
export * from "./mongodb-credentials-rotation-types";
|
||||
@@ -0,0 +1,27 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const MONGODB_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "MongoDB Credentials",
|
||||
type: SecretRotation.MongoDBCredentials,
|
||||
connection: AppConnection.MongoDB,
|
||||
template: {
|
||||
createUserStatement: `use [DATABASE_NAME]
|
||||
db.createUser({
|
||||
user: "infisical_user_1",
|
||||
pwd: "temporary_password",
|
||||
roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }]
|
||||
})
|
||||
|
||||
db.createUser({
|
||||
user: "infisical_user_2",
|
||||
pwd: "temporary_password",
|
||||
roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }]
|
||||
})`,
|
||||
secretsMapping: {
|
||||
username: "MONGODB_DB_USERNAME",
|
||||
password: "MONGODB_DB_PASSWORD"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { createMongoClient } from "@app/services/app-connection/mongodb/mongodb-connection-fns";
|
||||
|
||||
import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils";
|
||||
import {
|
||||
TMongoDBCredentialsRotationGeneratedCredentials,
|
||||
TMongoDBCredentialsRotationWithConnection
|
||||
} from "./mongodb-credentials-rotation-types";
|
||||
|
||||
const redactPasswords = (e: unknown, credentials: TMongoDBCredentialsRotationGeneratedCredentials) => {
|
||||
const error = e as Error;
|
||||
|
||||
if (!error?.message) return "Unknown error";
|
||||
|
||||
let redactedMessage = error.message;
|
||||
|
||||
credentials.forEach(({ password }) => {
|
||||
redactedMessage = redactedMessage.replaceAll(password, "*******************");
|
||||
});
|
||||
|
||||
return redactedMessage;
|
||||
};
|
||||
|
||||
export const mongodbCredentialsRotationFactory: TRotationFactory<
|
||||
TMongoDBCredentialsRotationWithConnection,
|
||||
TMongoDBCredentialsRotationGeneratedCredentials
|
||||
> = (secretRotation) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { username1, username2 },
|
||||
activeIndex,
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const passwordRequirement = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const $getClient = async () => {
|
||||
let client: MongoClient | null = null;
|
||||
try {
|
||||
client = await createMongoClient(connection.credentials, { validateConnection: true });
|
||||
return client;
|
||||
} catch (err) {
|
||||
if (client) await client.close();
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const $validateCredentials = async (credentials: TMongoDBCredentialsRotationGeneratedCredentials[number]) => {
|
||||
let client: MongoClient | null = null;
|
||||
try {
|
||||
client = await createMongoClient(connection.credentials, {
|
||||
authCredentials: {
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
},
|
||||
validateConnection: true
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, [credentials]));
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TMongoDBCredentialsRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
// For MongoDB, since we get existing users, we change both their passwords
|
||||
// on issue to invalidate their existing passwords
|
||||
const credentialsSet = [
|
||||
{ username: username1, password: generatePassword(passwordRequirement) },
|
||||
{ username: username2, password: generatePassword(passwordRequirement) }
|
||||
];
|
||||
|
||||
let client: MongoClient | null = null;
|
||||
try {
|
||||
client = await $getClient();
|
||||
const db = client.db(connection.credentials.database);
|
||||
|
||||
for (const credentials of credentialsSet) {
|
||||
await db.command({
|
||||
updateUser: credentials.username,
|
||||
pwd: credentials.password
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, credentialsSet));
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
|
||||
for (const credentials of credentialsSet) {
|
||||
await $validateCredentials(credentials);
|
||||
}
|
||||
|
||||
return callback(credentialsSet[0]);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TMongoDBCredentialsRotationGeneratedCredentials> = async (
|
||||
credentialsToRevoke,
|
||||
callback
|
||||
) => {
|
||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
|
||||
username,
|
||||
password: generatePassword(passwordRequirement)
|
||||
}));
|
||||
|
||||
let client: MongoClient | null = null;
|
||||
try {
|
||||
client = await $getClient();
|
||||
const db = client.db(connection.credentials.database);
|
||||
|
||||
for (const credentials of revokedCredentials) {
|
||||
await db.command({
|
||||
updateUser: credentials.username,
|
||||
pwd: credentials.password
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, revokedCredentials));
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TMongoDBCredentialsRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const credentials = {
|
||||
username: activeIndex === 0 ? username2 : username1,
|
||||
password: generatePassword(passwordRequirement)
|
||||
};
|
||||
|
||||
let client: MongoClient | null = null;
|
||||
try {
|
||||
client = await $getClient();
|
||||
const db = client.db(connection.credentials.database);
|
||||
|
||||
await db.command({
|
||||
updateUser: credentials.username,
|
||||
pwd: credentials.password
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(redactPasswords(error, [credentials]));
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
|
||||
await $validateCredentials(credentials);
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TMongoDBCredentialsRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const { username, password } = secretsMapping;
|
||||
|
||||
const secrets = [
|
||||
{
|
||||
key: username,
|
||||
value: generatedCredentials.username
|
||||
},
|
||||
{
|
||||
key: password,
|
||||
value: generatedCredentials.password
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import {
|
||||
SqlCredentialsRotationGeneratedCredentialsSchema,
|
||||
SqlCredentialsRotationParametersSchema,
|
||||
SqlCredentialsRotationTemplateSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const MongoDBCredentialsRotationGeneratedCredentialsSchema = SqlCredentialsRotationGeneratedCredentialsSchema;
|
||||
export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema;
|
||||
export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema;
|
||||
|
||||
const MongoDBCredentialsRotationSecretsMappingSchema = z.object({
|
||||
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.username),
|
||||
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.password)
|
||||
});
|
||||
|
||||
export const MongoDBCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MongoDBCredentials).extend({
|
||||
type: z.literal(SecretRotation.MongoDBCredentials),
|
||||
parameters: MongoDBCredentialsRotationParametersSchema,
|
||||
secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateMongoDBCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.MongoDBCredentials
|
||||
).extend({
|
||||
parameters: MongoDBCredentialsRotationParametersSchema,
|
||||
secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateMongoDBCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.MongoDBCredentials
|
||||
).extend({
|
||||
parameters: MongoDBCredentialsRotationParametersSchema.optional(),
|
||||
secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const MongoDBCredentialsRotationListItemSchema = z.object({
|
||||
name: z.literal("MongoDB Credentials"),
|
||||
connection: z.literal(AppConnection.MongoDB),
|
||||
type: z.literal(SecretRotation.MongoDBCredentials),
|
||||
template: MongoDBCredentialsRotationTemplateSchema
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TMongoDBConnection } from "@app/services/app-connection/mongodb";
|
||||
|
||||
import {
|
||||
CreateMongoDBCredentialsRotationSchema,
|
||||
MongoDBCredentialsRotationGeneratedCredentialsSchema,
|
||||
MongoDBCredentialsRotationListItemSchema,
|
||||
MongoDBCredentialsRotationSchema
|
||||
} from "./mongodb-credentials-rotation-schemas";
|
||||
|
||||
export type TMongoDBCredentialsRotation = z.infer<typeof MongoDBCredentialsRotationSchema>;
|
||||
|
||||
export type TMongoDBCredentialsRotationInput = z.infer<typeof CreateMongoDBCredentialsRotationSchema>;
|
||||
|
||||
export type TMongoDBCredentialsRotationListItem = z.infer<typeof MongoDBCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TMongoDBCredentialsRotationWithConnection = TMongoDBCredentialsRotation & {
|
||||
connection: TMongoDBConnection;
|
||||
};
|
||||
|
||||
export type TMongoDBCredentialsRotationGeneratedCredentials = z.infer<
|
||||
typeof MongoDBCredentialsRotationGeneratedCredentialsSchema
|
||||
>;
|
||||
@@ -8,7 +8,8 @@ export enum SecretRotation {
|
||||
AwsIamUserSecret = "aws-iam-user-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
OktaClientSecret = "okta-client-secret",
|
||||
RedisCredentials = "redis-credentials"
|
||||
RedisCredentials = "redis-credentials",
|
||||
MongoDBCredentials = "mongodb-credentials"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"
|
||||
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
||||
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
|
||||
import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
|
||||
import { MONGODB_CREDENTIALS_ROTATION_LIST_OPTION } from "./mongodb-credentials";
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
|
||||
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
|
||||
@@ -37,7 +38,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION
|
||||
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MongoDBCredentials]: MONGODB_CREDENTIALS_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
|
||||
@@ -11,7 +11,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
|
||||
[SecretRotation.LdapPassword]: "LDAP Password",
|
||||
[SecretRotation.OktaClientSecret]: "Okta Client Secret",
|
||||
[SecretRotation.RedisCredentials]: "Redis Credentials"
|
||||
[SecretRotation.RedisCredentials]: "Redis Credentials",
|
||||
[SecretRotation.MongoDBCredentials]: "MongoDB Credentials"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis,
|
||||
[SecretRotation.MongoDBCredentials]: AppConnection.MongoDB
|
||||
};
|
||||
|
||||
@@ -84,6 +84,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
|
||||
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
|
||||
import { mongodbCredentialsRotationFactory } from "./mongodb-credentials/mongodb-credentials-rotation-fns";
|
||||
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
|
||||
import { redisCredentialsRotationFactory } from "./redis-credentials/redis-credentials-rotation-fns";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
@@ -134,7 +135,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation
|
||||
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MongoDBCredentials]: mongodbCredentialsRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
|
||||
@@ -35,6 +35,12 @@ import {
|
||||
TLdapPasswordRotationListItem,
|
||||
TLdapPasswordRotationWithConnection
|
||||
} from "./ldap-password";
|
||||
import {
|
||||
TMongoDBCredentialsRotation,
|
||||
TMongoDBCredentialsRotationInput,
|
||||
TMongoDBCredentialsRotationListItem,
|
||||
TMongoDBCredentialsRotationWithConnection
|
||||
} from "./mongodb-credentials";
|
||||
import {
|
||||
TMsSqlCredentialsRotation,
|
||||
TMsSqlCredentialsRotationInput,
|
||||
@@ -86,7 +92,8 @@ export type TSecretRotationV2 =
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation
|
||||
| TOktaClientSecretRotation
|
||||
| TRedisCredentialsRotation;
|
||||
| TRedisCredentialsRotation
|
||||
| TMongoDBCredentialsRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
@@ -98,7 +105,8 @@ export type TSecretRotationV2WithConnection =
|
||||
| TLdapPasswordRotationWithConnection
|
||||
| TAwsIamUserSecretRotationWithConnection
|
||||
| TOktaClientSecretRotationWithConnection
|
||||
| TRedisCredentialsRotationWithConnection;
|
||||
| TRedisCredentialsRotationWithConnection
|
||||
| TMongoDBCredentialsRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
@@ -119,7 +127,8 @@ export type TSecretRotationV2Input =
|
||||
| TLdapPasswordRotationInput
|
||||
| TAwsIamUserSecretRotationInput
|
||||
| TOktaClientSecretRotationInput
|
||||
| TRedisCredentialsRotationInput;
|
||||
| TRedisCredentialsRotationInput
|
||||
| TMongoDBCredentialsRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
@@ -131,7 +140,8 @@ export type TSecretRotationV2ListItem =
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem
|
||||
| TOktaClientSecretRotationListItem
|
||||
| TRedisCredentialsRotationListItem;
|
||||
| TRedisCredentialsRotationListItem
|
||||
| TMongoDBCredentialsRotationListItem;
|
||||
|
||||
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotatio
|
||||
import { AwsIamUserSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MongoDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mongodb-credentials";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
|
||||
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
|
||||
@@ -21,5 +22,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
OktaClientSecretRotationSchema,
|
||||
RedisCredentialsRotationSchema
|
||||
RedisCredentialsRotationSchema,
|
||||
MongoDBCredentialsRotationSchema
|
||||
]);
|
||||
|
||||
@@ -85,8 +85,6 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
// For SQL, since we get existing users, we change both their passwords
|
||||
// on issue to invalidate their existing passwords
|
||||
// For SQL, since we get existing users, we change both their passwords
|
||||
// on issue to invalidate their existing passwords
|
||||
const credentialsSet = [
|
||||
|
||||
@@ -2874,6 +2874,12 @@ export const SecretRotations = {
|
||||
},
|
||||
REDIS_CREDENTIALS: {
|
||||
permissionScope: "The ACL permission scope to assign to the issued Redis users."
|
||||
},
|
||||
MONGODB_CREDENTIALS: {
|
||||
username1:
|
||||
"The username of the first MongoDB user to rotate passwords for. This user must already exist in your database.",
|
||||
username2:
|
||||
"The username of the second MongoDB user to rotate passwords for. This user must already exist in your database."
|
||||
}
|
||||
},
|
||||
SECRETS_MAPPING: {
|
||||
@@ -2904,6 +2910,10 @@ export const SecretRotations = {
|
||||
OKTA_CLIENT_SECRET: {
|
||||
clientId: "The name of the secret that the client ID will be mapped to.",
|
||||
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||
},
|
||||
MONGODB_CREDENTIALS: {
|
||||
username: "The name of the secret that the active username will be mapped to.",
|
||||
password: "The name of the secret that the generated password will be mapped to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,6 +286,10 @@ const envSchema = z
|
||||
DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default(
|
||||
process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY
|
||||
),
|
||||
|
||||
// PAM AWS credentials (for AWS IAM PAM resource type)
|
||||
PAM_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||
PAM_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()),
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
|
||||
@@ -183,3 +183,23 @@ export class CryptographyError extends Error {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export class PolicyViolationError extends Error {
|
||||
name: string;
|
||||
|
||||
error: unknown;
|
||||
|
||||
details?: unknown;
|
||||
|
||||
constructor({
|
||||
name,
|
||||
error,
|
||||
message,
|
||||
details
|
||||
}: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) {
|
||||
super(message || "A policy is in place for this resource");
|
||||
this.name = name || "PolicyViolationError";
|
||||
this.error = error;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ export enum QueueName {
|
||||
HealthAlert = "health-alert",
|
||||
CertificateV3AutoRenewal = "certificate-v3-auto-renewal",
|
||||
PamAccountRotation = "pam-account-rotation",
|
||||
PamSessionExpiration = "pam-session-expiration",
|
||||
PkiAcmeChallengeValidation = "pki-acme-challenge-validation"
|
||||
}
|
||||
|
||||
@@ -138,6 +139,7 @@ export enum QueueJobs {
|
||||
HealthAlert = "health-alert",
|
||||
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal",
|
||||
PamAccountRotation = "pam-account-rotation",
|
||||
PamSessionExpiration = "pam-session-expiration",
|
||||
PkiAcmeChallengeValidation = "pki-acme-challenge-validation"
|
||||
}
|
||||
|
||||
@@ -404,6 +406,10 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.PamAccountRotation;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.PamSessionExpiration]: {
|
||||
name: QueueJobs.PamSessionExpiration;
|
||||
payload: { sessionId: string };
|
||||
};
|
||||
[QueueName.PkiAcmeChallengeValidation]: {
|
||||
name: QueueJobs.PkiAcmeChallengeValidation;
|
||||
payload: { challengeId: string };
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
NotFoundError,
|
||||
OidcAuthError,
|
||||
PermissionBoundaryError,
|
||||
PolicyViolationError,
|
||||
RateLimitError,
|
||||
ScimRequestError,
|
||||
UnauthorizedError
|
||||
@@ -255,6 +256,14 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
detail: error.message
|
||||
// TODO: add subproblems if they exist
|
||||
});
|
||||
} else if (error instanceof PolicyViolationError) {
|
||||
void res.status(HttpStatusCodes.Forbidden).send({
|
||||
reqId: req.id,
|
||||
statusCode: HttpStatusCodes.Forbidden,
|
||||
error: "PolicyViolationError",
|
||||
message: error.message,
|
||||
details: error.details
|
||||
});
|
||||
} else {
|
||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||
reqId: req.id,
|
||||
|
||||
@@ -159,6 +159,19 @@ import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||
import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import {
|
||||
approvalPolicyDALFactory,
|
||||
approvalPolicyStepApproversDALFactory,
|
||||
approvalPolicyStepsDALFactory
|
||||
} from "@app/services/approval-policy/approval-policy-dal";
|
||||
import { approvalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service";
|
||||
import {
|
||||
approvalRequestApprovalsDALFactory,
|
||||
approvalRequestDALFactory,
|
||||
approvalRequestGrantsDALFactory,
|
||||
approvalRequestStepEligibleApproversDALFactory,
|
||||
approvalRequestStepsDALFactory
|
||||
} from "@app/services/approval-policy/approval-request-dal";
|
||||
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||
@@ -279,6 +292,7 @@ import { orgServiceFactory } from "@app/services/org/org-service";
|
||||
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue";
|
||||
import { pamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue";
|
||||
import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue";
|
||||
import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal";
|
||||
import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
@@ -1916,6 +1930,9 @@ export const registerRoutes = async (
|
||||
identityDAL
|
||||
});
|
||||
|
||||
const approvalRequestDAL = approvalRequestDALFactory(db);
|
||||
const approvalRequestGrantsDAL = approvalRequestGrantsDALFactory(db);
|
||||
|
||||
// DAILY
|
||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||
scimService,
|
||||
@@ -1931,7 +1948,9 @@ export const registerRoutes = async (
|
||||
serviceTokenService,
|
||||
orgService,
|
||||
userNotificationDAL,
|
||||
keyValueStoreDAL
|
||||
keyValueStoreDAL,
|
||||
approvalRequestDAL,
|
||||
approvalRequestGrantsDAL
|
||||
});
|
||||
|
||||
const healthAlert = healthAlertServiceFactory({
|
||||
@@ -2412,6 +2431,12 @@ export const registerRoutes = async (
|
||||
gatewayV2Service
|
||||
});
|
||||
|
||||
const approvalPolicyDAL = approvalPolicyDALFactory(db);
|
||||
const pamSessionExpirationService = pamSessionExpirationServiceFactory({
|
||||
queueService,
|
||||
pamSessionDAL
|
||||
});
|
||||
|
||||
const pamAccountService = pamAccountServiceFactory({
|
||||
pamAccountDAL,
|
||||
gatewayV2Service,
|
||||
@@ -2423,7 +2448,10 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
projectDAL,
|
||||
userDAL,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
approvalRequestGrantsDAL,
|
||||
approvalPolicyDAL,
|
||||
pamSessionExpirationService
|
||||
});
|
||||
|
||||
const pamAccountRotation = pamAccountRotationServiceFactory({
|
||||
@@ -2451,6 +2479,27 @@ export const registerRoutes = async (
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const approvalPolicyStepsDAL = approvalPolicyStepsDALFactory(db);
|
||||
const approvalPolicyStepApproversDAL = approvalPolicyStepApproversDALFactory(db);
|
||||
const approvalRequestStepsDAL = approvalRequestStepsDALFactory(db);
|
||||
const approvalRequestStepEligibleApproversDAL = approvalRequestStepEligibleApproversDALFactory(db);
|
||||
const approvalRequestApprovalsDAL = approvalRequestApprovalsDALFactory(db);
|
||||
|
||||
const approvalPolicyService = approvalPolicyServiceFactory({
|
||||
approvalPolicyDAL,
|
||||
approvalPolicyStepsDAL,
|
||||
approvalPolicyStepApproversDAL,
|
||||
permissionService,
|
||||
projectMembershipDAL,
|
||||
approvalRequestDAL,
|
||||
approvalRequestStepsDAL,
|
||||
approvalRequestStepEligibleApproversDAL,
|
||||
approvalRequestApprovalsDAL,
|
||||
userGroupMembershipDAL,
|
||||
notificationService,
|
||||
approvalRequestGrantsDAL
|
||||
});
|
||||
|
||||
// setup the communication with license key server
|
||||
await licenseService.init();
|
||||
|
||||
@@ -2490,6 +2539,7 @@ export const registerRoutes = async (
|
||||
await healthAlert.init();
|
||||
await pkiSyncCleanup.init();
|
||||
await pamAccountRotation.init();
|
||||
await pamSessionExpirationService.init();
|
||||
await dailyReminderQueueService.startDailyRemindersJob();
|
||||
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
@@ -2630,7 +2680,8 @@ export const registerRoutes = async (
|
||||
additionalPrivilege: additionalPrivilegeService,
|
||||
identityProject: identityProjectService,
|
||||
convertor: convertorService,
|
||||
pkiAlertV2: pkiAlertV2Service
|
||||
pkiAlertV2: pkiAlertV2Service,
|
||||
approvalPolicy: approvalPolicyService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
@@ -2,6 +2,8 @@ import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas";
|
||||
|
||||
import { UnpackedPermissionSchema } from "./permission";
|
||||
|
||||
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
|
||||
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.omit({
|
||||
projectMembershipId: true
|
||||
}).extend({
|
||||
permissions: UnpackedPermissionSchema.array()
|
||||
});
|
||||
|
||||
@@ -87,6 +87,10 @@ import {
|
||||
SanitizedLaravelForgeConnectionSchema
|
||||
} from "@app/services/app-connection/laravel-forge";
|
||||
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||
import {
|
||||
MongoDBConnectionListItemSchema,
|
||||
SanitizedMongoDBConnectionSchema
|
||||
} from "@app/services/app-connection/mongodb";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
|
||||
import {
|
||||
@@ -173,6 +177,7 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedMongoDBConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options,
|
||||
...SanitizedDNSMadeEasyConnectionSchema.options
|
||||
@@ -219,6 +224,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
MongoDBConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema,
|
||||
DNSMadeEasyConnectionListItemSchema
|
||||
|
||||
@@ -16,8 +16,8 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
@@ -28,6 +28,7 @@ import { registerHerokuConnectionRouter } from "./heroku-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerLaravelForgeConnectionRouter } from "./laravel-forge-connection-router";
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMongoDBConnectionRouter } from "./mongodb-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
|
||||
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
||||
@@ -90,5 +91,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter,
|
||||
[AppConnection.MongoDB]: registerMongoDBConnectionRouter,
|
||||
[AppConnection.Chef]: registerChefConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateMongoDBConnectionSchema,
|
||||
SanitizedMongoDBConnectionSchema,
|
||||
UpdateMongoDBConnectionSchema
|
||||
} from "@app/services/app-connection/mongodb";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerMongoDBConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.MongoDB,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedMongoDBConnectionSchema,
|
||||
createSchema: CreateMongoDBConnectionSchema,
|
||||
updateSchema: UpdateMongoDBConnectionSchema
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,625 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||
import {
|
||||
TApprovalPolicy,
|
||||
TCreatePolicyDTO,
|
||||
TCreateRequestDTO,
|
||||
TUpdatePolicyDTO
|
||||
} from "@app/services/approval-policy/approval-policy-types";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerApprovalPolicyEndpoints = <P extends TApprovalPolicy>({
|
||||
server,
|
||||
policyType,
|
||||
createPolicySchema,
|
||||
updatePolicySchema,
|
||||
policyResponseSchema,
|
||||
createRequestSchema,
|
||||
requestResponseSchema,
|
||||
grantResponseSchema
|
||||
}: {
|
||||
server: FastifyZodProvider;
|
||||
policyType: ApprovalPolicyType;
|
||||
createPolicySchema: z.ZodType<
|
||||
TCreatePolicyDTO & {
|
||||
conditions: P["conditions"]["conditions"];
|
||||
constraints: P["constraints"]["constraints"];
|
||||
}
|
||||
>;
|
||||
updatePolicySchema: z.ZodType<
|
||||
TUpdatePolicyDTO & {
|
||||
conditions?: P["conditions"]["conditions"];
|
||||
constraints?: P["constraints"]["constraints"];
|
||||
}
|
||||
>;
|
||||
policyResponseSchema: z.ZodTypeAny;
|
||||
createRequestSchema: z.ZodType<TCreateRequestDTO>;
|
||||
requestResponseSchema: z.ZodTypeAny;
|
||||
grantResponseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
// Policies
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create approval policy",
|
||||
body: createPolicySchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
policy: policyResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { policy } = await server.services.approvalPolicy.create(policyType, req.body, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: req.body.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_POLICY_CREATE,
|
||||
metadata: {
|
||||
policyType,
|
||||
name: req.body.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { policy };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List approval policies",
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
policies: z.array(policyResponseSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { policies } = await server.services.approvalPolicy.list(policyType, req.query.projectId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_POLICY_LIST,
|
||||
metadata: {
|
||||
policyType,
|
||||
count: policies.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { policies };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:policyId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get approval policy",
|
||||
params: z.object({
|
||||
policyId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
policy: policyResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { policy } = await server.services.approvalPolicy.getById(req.params.policyId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: policy.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_POLICY_GET,
|
||||
metadata: {
|
||||
policyType,
|
||||
policyId: policy.id,
|
||||
name: policy.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { policy };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:policyId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Update approval policy",
|
||||
params: z.object({
|
||||
policyId: z.string().uuid()
|
||||
}),
|
||||
body: updatePolicySchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
policy: policyResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { policy } = await server.services.approvalPolicy.updateById(req.params.policyId, req.body, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: policy.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_POLICY_UPDATE,
|
||||
metadata: {
|
||||
policyType,
|
||||
policyId: policy.id,
|
||||
name: policy.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { policy };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:policyId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Delete approval policy",
|
||||
params: z.object({
|
||||
policyId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
policyId: z.string().uuid()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { policyId, projectId } = await server.services.approvalPolicy.deleteById(
|
||||
req.params.policyId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_POLICY_DELETE,
|
||||
metadata: {
|
||||
policyType,
|
||||
policyId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { policyId };
|
||||
}
|
||||
});
|
||||
|
||||
// Requests
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/requests",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List approval requests",
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
requests: z.array(requestResponseSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { requests } = await server.services.approvalPolicy.listRequests(
|
||||
policyType,
|
||||
req.query.projectId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_LIST,
|
||||
metadata: {
|
||||
policyType,
|
||||
count: requests.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { requests };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create approval request",
|
||||
body: createRequestSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
// To prevent type errors when accessing req.auth.user
|
||||
if (req.auth.authMode !== AuthMode.JWT) {
|
||||
throw new BadRequestError({ message: "You can only request access using JWT auth tokens." });
|
||||
}
|
||||
|
||||
const { request } = await server.services.approvalPolicy.createRequest(
|
||||
policyType,
|
||||
{
|
||||
requesterName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(),
|
||||
requesterEmail: req.auth.user.email ?? "",
|
||||
...req.body
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: request.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_CREATE,
|
||||
metadata: {
|
||||
policyType,
|
||||
justification: req.body.justification || undefined,
|
||||
requestDuration: req.body.requestDuration || "infinite"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/requests/:requestId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.getRequestById(req.params.requestId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: request.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_GET,
|
||||
metadata: {
|
||||
policyType,
|
||||
requestId: request.id,
|
||||
status: request.status
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests/:requestId/approve",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Approve approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
comment: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.approveRequest(
|
||||
req.params.requestId,
|
||||
req.body,
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: request.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_APPROVE,
|
||||
metadata: {
|
||||
policyType,
|
||||
requestId: req.params.requestId,
|
||||
comment: req.body.comment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests/:requestId/reject",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Reject approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
comment: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.rejectRequest(
|
||||
req.params.requestId,
|
||||
req.body,
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: request.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_REJECT,
|
||||
metadata: {
|
||||
policyType,
|
||||
requestId: req.params.requestId,
|
||||
comment: req.body.comment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/requests/:requestId/cancel",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Cancel approval request",
|
||||
params: z.object({
|
||||
requestId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
request: requestResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { request } = await server.services.approvalPolicy.cancelRequest(req.params.requestId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: request.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_CANCEL,
|
||||
metadata: {
|
||||
policyType,
|
||||
requestId: req.params.requestId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { request };
|
||||
}
|
||||
});
|
||||
|
||||
// Grants
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/grants",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List approval grants",
|
||||
querystring: z.object({
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
grants: z.array(grantResponseSchema)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { grants } = await server.services.approvalPolicy.listGrants(
|
||||
policyType,
|
||||
req.query.projectId,
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_GRANT_LIST,
|
||||
metadata: {
|
||||
policyType,
|
||||
count: grants.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { grants };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/grants/:grantId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Get approval grant",
|
||||
params: z.object({
|
||||
grantId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
grant: grantResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { grant } = await server.services.approvalPolicy.getGrantById(req.params.grantId, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: grant.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_GRANT_GET,
|
||||
metadata: {
|
||||
policyType,
|
||||
grantId: grant.id,
|
||||
status: grant.status
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { grant };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/grants/:grantId/revoke",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Revoke approval grant",
|
||||
params: z.object({
|
||||
grantId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
revocationReason: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
grant: grantResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { grant } = await server.services.approvalPolicy.revokeGrant(req.params.grantId, req.body, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: grant.projectId,
|
||||
event: {
|
||||
type: EventType.APPROVAL_REQUEST_GRANT_REVOKE,
|
||||
metadata: {
|
||||
policyType,
|
||||
grantId: grant.id,
|
||||
revocationReason: req.body.revocationReason
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { grant };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums";
|
||||
import {
|
||||
CreatePamAccessPolicySchema,
|
||||
CreatePamAccessRequestSchema,
|
||||
PamAccessPolicySchema,
|
||||
PamAccessRequestGrantSchema,
|
||||
PamAccessRequestSchema,
|
||||
UpdatePamAccessPolicySchema
|
||||
} from "@app/services/approval-policy/pam-access/pam-access-policy-schemas";
|
||||
|
||||
import { registerApprovalPolicyEndpoints } from "./approval-policy-endpoints";
|
||||
|
||||
export const APPROVAL_POLICY_REGISTER_ROUTER_MAP: Record<
|
||||
ApprovalPolicyType,
|
||||
(server: FastifyZodProvider) => Promise<void>
|
||||
> = {
|
||||
[ApprovalPolicyType.PamAccess]: async (server: FastifyZodProvider) => {
|
||||
registerApprovalPolicyEndpoints({
|
||||
server,
|
||||
policyType: ApprovalPolicyType.PamAccess,
|
||||
createPolicySchema: CreatePamAccessPolicySchema,
|
||||
updatePolicySchema: UpdatePamAccessPolicySchema,
|
||||
policyResponseSchema: PamAccessPolicySchema,
|
||||
createRequestSchema: CreatePamAccessRequestSchema,
|
||||
requestResponseSchema: PamAccessRequestSchema,
|
||||
grantResponseSchema: PamAccessRequestGrantSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router"
|
||||
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
|
||||
|
||||
import { registerAdminRouter } from "./admin-router";
|
||||
import { APPROVAL_POLICY_REGISTER_ROUTER_MAP } from "./approval-policy-routers";
|
||||
import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
@@ -275,4 +276,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.register(registerEventRouter, { prefix: "/events" });
|
||||
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
|
||||
|
||||
await server.register(
|
||||
async (approvalPolicyRouter) => {
|
||||
// Register policy type-specific endpoints
|
||||
for await (const [type, router] of Object.entries(APPROVAL_POLICY_REGISTER_ROUTER_MAP)) {
|
||||
await approvalPolicyRouter.register(router, { prefix: `/${type}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/approval-policies" }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,7 +79,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,7 +106,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -136,7 +142,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,7 +167,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -179,7 +191,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
|
||||
const additionalPrivilege = await additionalPrivilegeDAL.deleteById(existingPrivilege.id);
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -199,7 +214,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` });
|
||||
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -219,7 +237,10 @@ export const additionalPrivilegeServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Additional privilege with name ${selector.name} doesn't exist` });
|
||||
|
||||
return {
|
||||
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
|
||||
additionalPrivilege: {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum AppConnection {
|
||||
Netlify = "netlify",
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
MongoDB = "mongodb",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef",
|
||||
Northflank = "northflank"
|
||||
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
validateLaravelForgeConnectionCredentials
|
||||
} from "./laravel-forge";
|
||||
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
|
||||
import { getMongoDBConnectionListItem, MongoDBConnectionMethod, validateMongoDBConnectionCredentials } from "./mongodb";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
|
||||
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
|
||||
@@ -224,6 +225,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getNorthflankConnectionListItem(),
|
||||
getOktaConnectionListItem(),
|
||||
getRedisConnectionListItem(),
|
||||
getMongoDBConnectionListItem(),
|
||||
getChefConnectionListItem()
|
||||
]
|
||||
.filter((option) => {
|
||||
@@ -357,7 +359,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
|
||||
@@ -411,6 +414,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
case RedisConnectionMethod.UsernameAndPassword:
|
||||
case MongoDBConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
@@ -504,6 +508,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.MongoDB]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta",
|
||||
[AppConnection.Redis]: "Redis",
|
||||
[AppConnection.MongoDB]: "MongoDB",
|
||||
[AppConnection.Chef]: "Chef",
|
||||
[AppConnection.Northflank]: "Northflank"
|
||||
};
|
||||
@@ -88,6 +89,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.MongoDB]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Chef]: AppConnectionPlanType.Enterprise,
|
||||
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -72,11 +72,11 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
|
||||
import { flyioConnectionService } from "./flyio/flyio-connection-service";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
@@ -96,6 +96,7 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
|
||||
import { ValidateLaravelForgeConnectionCredentialsSchema } from "./laravel-forge";
|
||||
import { laravelForgeConnectionService } from "./laravel-forge/laravel-forge-connection-service";
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMongoDBConnectionCredentialsSchema } from "./mongodb";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
|
||||
@@ -180,6 +181,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||
[AppConnection.MongoDB]: ValidateMongoDBConnectionCredentialsSchema,
|
||||
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
TOracleDBConnectionInput,
|
||||
TValidateOracleDBConnectionCredentialsSchema
|
||||
} from "@app/ee/services/app-connections/oracledb";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -172,6 +172,12 @@ import {
|
||||
TLdapConnectionInput,
|
||||
TValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap";
|
||||
import {
|
||||
TMongoDBConnection,
|
||||
TMongoDBConnectionConfig,
|
||||
TMongoDBConnectionInput,
|
||||
TValidateMongoDBConnectionCredentialsSchema
|
||||
} from "./mongodb";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import {
|
||||
@@ -295,6 +301,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TMongoDBConnection
|
||||
| TChefConnection
|
||||
);
|
||||
|
||||
@@ -345,6 +352,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TNorthflankConnectionInput
|
||||
| TOktaConnectionInput
|
||||
| TRedisConnectionInput
|
||||
| TMongoDBConnectionInput
|
||||
| TChefConnectionInput
|
||||
);
|
||||
|
||||
@@ -413,6 +421,7 @@ export type TAppConnectionConfig =
|
||||
| TNorthflankConnectionConfig
|
||||
| TOktaConnectionConfig
|
||||
| TRedisConnectionConfig
|
||||
| TMongoDBConnectionConfig
|
||||
| TChefConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
@@ -458,6 +467,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateNorthflankConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema
|
||||
| TValidateRedisConnectionCredentialsSchema
|
||||
| TValidateMongoDBConnectionCredentialsSchema
|
||||
| TValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
|
||||
4
backend/src/services/app-connection/mongodb/index.ts
Normal file
4
backend/src/services/app-connection/mongodb/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./mongodb-connection-enums";
|
||||
export * from "./mongodb-connection-fns";
|
||||
export * from "./mongodb-connection-schemas";
|
||||
export * from "./mongodb-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum MongoDBConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { MongoDBConnectionMethod } from "./mongodb-connection-enums";
|
||||
import { TMongoDBConnectionConfig } from "./mongodb-connection-types";
|
||||
|
||||
export const getMongoDBConnectionListItem = () => {
|
||||
return {
|
||||
name: "MongoDB" as const,
|
||||
app: AppConnection.MongoDB as const,
|
||||
methods: Object.values(MongoDBConnectionMethod) as [MongoDBConnectionMethod.UsernameAndPassword],
|
||||
supportsPlatformManagement: false as const
|
||||
};
|
||||
};
|
||||
|
||||
export type TMongoDBConnectionCredentials = {
|
||||
host: string;
|
||||
port?: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
tlsEnabled?: boolean;
|
||||
tlsRejectUnauthorized?: boolean;
|
||||
tlsCertificate?: string;
|
||||
};
|
||||
|
||||
export type TCreateMongoClientOptions = {
|
||||
authCredentials?: { username: string; password: string };
|
||||
validateConnection?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000;
|
||||
|
||||
export const createMongoClient = async (
|
||||
credentials: TMongoDBConnectionCredentials,
|
||||
options?: TCreateMongoClientOptions
|
||||
): Promise<MongoClient> => {
|
||||
const srvRegex = new RE2("^mongodb\\+srv:\\/\\/");
|
||||
const protocolRegex = new RE2("^mongodb:\\/\\/");
|
||||
|
||||
let normalizedHost = credentials.host.trim();
|
||||
const isSrvFromHost = srvRegex.test(normalizedHost);
|
||||
if (isSrvFromHost) {
|
||||
normalizedHost = srvRegex.replace(normalizedHost, "");
|
||||
} else if (protocolRegex.test(normalizedHost)) {
|
||||
normalizedHost = protocolRegex.replace(normalizedHost, "");
|
||||
}
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(normalizedHost);
|
||||
|
||||
const isSrv = !credentials.port || isSrvFromHost;
|
||||
const uri = isSrv ? `mongodb+srv://${hostIp}` : `mongodb://${hostIp}:${credentials.port}`;
|
||||
|
||||
const authCredentials = options?.authCredentials ?? {
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
};
|
||||
|
||||
const clientOptions: {
|
||||
auth?: { username: string; password?: string };
|
||||
authSource?: string;
|
||||
tls?: boolean;
|
||||
tlsInsecure?: boolean;
|
||||
ca?: string;
|
||||
directConnection?: boolean;
|
||||
connectTimeoutMS?: number;
|
||||
serverSelectionTimeoutMS?: number;
|
||||
socketTimeoutMS?: number;
|
||||
} = {
|
||||
auth: {
|
||||
username: authCredentials.username,
|
||||
password: authCredentials.password
|
||||
},
|
||||
authSource: isSrv ? undefined : credentials.database,
|
||||
directConnection: !isSrv,
|
||||
connectTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS,
|
||||
serverSelectionTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS,
|
||||
socketTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS
|
||||
};
|
||||
|
||||
if (credentials.tlsEnabled) {
|
||||
clientOptions.tls = true;
|
||||
clientOptions.tlsInsecure = !credentials.tlsRejectUnauthorized;
|
||||
if (credentials.tlsCertificate) {
|
||||
clientOptions.ca = credentials.tlsCertificate;
|
||||
}
|
||||
}
|
||||
|
||||
const client = new MongoClient(uri, clientOptions);
|
||||
|
||||
if (options?.validateConnection) {
|
||||
await client
|
||||
.db(credentials.database)
|
||||
.command({ ping: 1 })
|
||||
.then(() => true);
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
export const validateMongoDBConnectionCredentials = async (config: TMongoDBConnectionConfig) => {
|
||||
let client: MongoClient | null = null;
|
||||
try {
|
||||
client = await createMongoClient(config.credentials, { validateConnection: true });
|
||||
|
||||
if (client) await client.close();
|
||||
|
||||
return config.credentials;
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestError) {
|
||||
throw err;
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { MongoDBConnectionMethod } from "./mongodb-connection-enums";
|
||||
|
||||
export const BaseMongoDBUsernameAndPasswordConnectionSchema = z.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
database: z.string().min(1).trim(),
|
||||
|
||||
tlsRejectUnauthorized: z.boolean(),
|
||||
tlsEnabled: z.boolean(),
|
||||
tlsCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const MongoDBConnectionAccessTokenCredentialsSchema = BaseMongoDBUsernameAndPasswordConnectionSchema;
|
||||
|
||||
const BaseMongoDBConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.MongoDB) });
|
||||
|
||||
export const MongoDBConnectionSchema = BaseMongoDBConnectionSchema.extend({
|
||||
method: z.literal(MongoDBConnectionMethod.UsernameAndPassword),
|
||||
credentials: MongoDBConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedMongoDBConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseMongoDBConnectionSchema.extend({
|
||||
method: z.literal(MongoDBConnectionMethod.UsernameAndPassword),
|
||||
credentials: MongoDBConnectionAccessTokenCredentialsSchema.pick({
|
||||
host: true,
|
||||
port: true,
|
||||
username: true,
|
||||
database: true,
|
||||
tlsEnabled: true,
|
||||
tlsRejectUnauthorized: true,
|
||||
tlsCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateMongoDBConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(MongoDBConnectionMethod.UsernameAndPassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.MongoDB).method),
|
||||
credentials: MongoDBConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.MongoDB).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateMongoDBConnectionSchema = ValidateMongoDBConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.MongoDB, {
|
||||
supportsPlatformManagedCredentials: false,
|
||||
supportsGateways: false
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateMongoDBConnectionSchema = z
|
||||
.object({
|
||||
credentials: MongoDBConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.MongoDB).credentials
|
||||
)
|
||||
})
|
||||
.and(
|
||||
GenericUpdateAppConnectionFieldsSchema(AppConnection.MongoDB, {
|
||||
supportsPlatformManagedCredentials: false,
|
||||
supportsGateways: false
|
||||
})
|
||||
);
|
||||
|
||||
export const MongoDBConnectionListItemSchema = z.object({
|
||||
name: z.literal("MongoDB"),
|
||||
app: z.literal(AppConnection.MongoDB),
|
||||
methods: z.nativeEnum(MongoDBConnectionMethod).array(),
|
||||
supportsPlatformManagement: z.literal(false)
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateMongoDBConnectionSchema,
|
||||
MongoDBConnectionSchema,
|
||||
ValidateMongoDBConnectionCredentialsSchema
|
||||
} from "./mongodb-connection-schemas";
|
||||
|
||||
export type TMongoDBConnection = z.infer<typeof MongoDBConnectionSchema>;
|
||||
|
||||
export type TMongoDBConnectionInput = z.infer<typeof CreateMongoDBConnectionSchema> & {
|
||||
app: AppConnection.MongoDB;
|
||||
};
|
||||
|
||||
export type TValidateMongoDBConnectionCredentialsSchema = typeof ValidateMongoDBConnectionCredentialsSchema;
|
||||
|
||||
export type TMongoDBConnectionConfig = DiscriminativePick<TMongoDBConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user