Merge remote-tracking branch 'origin/main' into PAM-12-add-k8s-for-pam

This commit is contained in:
Sheen Capadngan
2025-12-11 01:09:11 +08:00
413 changed files with 15890 additions and 5719 deletions

3916
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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
>;
}
}

View File

@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasSubOrganizationIdColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "subOrganizationId");
if (!hasSubOrganizationIdColumn) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.uuid("subOrganizationId").nullable();
t.foreign("subOrganizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasSubOrganizationIdColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "subOrganizationId");
if (hasSubOrganizationIdColumn) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.dropColumn("subOrganizationId");
});
}
}

View 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);
}

View 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();
});
}
}

View 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>>;

View 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>
>;

View 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>>;

View File

@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const 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>
>;

View 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>>;

View 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 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>
>;

View 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>>;

View 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>>;

View File

@@ -22,7 +22,8 @@ export const IdentityAccessTokensSchema = z.object({
updatedAt: z.date(),
name: z.string().nullable().optional(),
authMethod: z.string(),
accessTokenPeriod: z.coerce.number().default(0)
accessTokenPeriod: z.coerce.number().default(0),
subOrganizationId: z.string().uuid().nullable().optional()
});
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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(),

View File

@@ -12,7 +12,6 @@ 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";

View File

@@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const plan = await server.services.license.getOrgPlan({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.rootOrgId,
actorOrgId: req.permission.orgId,
rootOrgId: req.permission.rootOrgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId,
refreshCache: req.query.refreshCache
@@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
orgId: req.params.organizationId,
rootOrgId: req.permission.rootOrgId
});
return data;
}

View File

@@ -1,3 +1,8 @@
import {
CreateAwsIamAccountSchema,
SanitizedAwsIamAccountWithResourceSchema,
UpdateAwsIamAccountSchema
} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas";
import {
CreateKubernetesAccountSchema,
SanitizedKubernetesAccountWithResourceSchema,
@@ -58,5 +63,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
createAccountSchema: CreateKubernetesAccountSchema,
updateAccountSchema: UpdateKubernetesAccountSchema
});
},
[PamResource.AwsIam]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.AwsIam,
accountResponseSchema: SanitizedAwsIamAccountWithResourceSchema,
createAccountSchema: CreateAwsIamAccountSchema,
updateAccountSchema: UpdateAwsIamAccountSchema
});
}
};

View File

@@ -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
}
}

View File

@@ -3,9 +3,11 @@ 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 { SanitizedKubernetesAccountWithResourceSchema } from "@app/ee/services/pam-resource/kubernetes/kubernetes-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";
@@ -20,7 +22,8 @@ const SanitizedAccountSchema = z.union([
SanitizedSSHAccountWithResourceSchema, // ORDER MATTERS
SanitizedPostgresAccountWithResourceSchema,
SanitizedMySQLAccountWithResourceSchema,
SanitizedKubernetesAccountWithResourceSchema
SanitizedKubernetesAccountWithResourceSchema,
SanitizedAwsIamAccountWithResourceSchema
]);
const ListPamAccountsResponseSchema = z.object({
@@ -129,18 +132,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]),
@@ -166,7 +170,7 @@ 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: {

View File

@@ -1,3 +1,8 @@
import {
CreateAwsIamResourceSchema,
SanitizedAwsIamResourceSchema,
UpdateAwsIamResourceSchema
} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas";
import {
CreateKubernetesResourceSchema,
SanitizedKubernetesResourceSchema,
@@ -58,5 +63,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
createResourceSchema: CreateKubernetesResourceSchema,
updateResourceSchema: UpdateKubernetesResourceSchema
});
},
[PamResource.AwsIam]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.AwsIam,
resourceResponseSchema: SanitizedAwsIamResourceSchema,
createResourceSchema: CreateAwsIamResourceSchema,
updateResourceSchema: UpdateAwsIamResourceSchema
});
}
};

View File

@@ -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 })
}
}
});

View File

@@ -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 {
KubernetesResourceListItemSchema,
SanitizedKubernetesResourceSchema
@@ -27,14 +31,16 @@ const SanitizedResourceSchema = z.union([
SanitizedPostgresResourceSchema,
SanitizedMySQLResourceSchema,
SanitizedSSHResourceSchema,
SanitizedKubernetesResourceSchema
SanitizedKubernetesResourceSchema,
SanitizedAwsIamResourceSchema
]);
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
PostgresResourceListItemSchema,
MySQLResourceListItemSchema,
SSHResourceListItemSchema,
KubernetesResourceListItemSchema
KubernetesResourceListItemSchema,
AwsIamResourceListItemSchema
]);
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {

View File

@@ -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()

View File

@@ -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
}))

View File

@@ -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
};

View File

@@ -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
});

View File

@@ -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) => {

View File

@@ -368,6 +368,7 @@ export enum EventType {
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
USER_LOGIN = "user-login",
SELECT_ORGANIZATION = "select-organization",
SELECT_SUB_ORGANIZATION = "select-sub-organization",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
@@ -559,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[] = [
@@ -2690,6 +2705,15 @@ interface SelectOrganizationEvent {
};
}
interface SelectSubOrganizationEvent {
type: EventType.SELECT_SUB_ORGANIZATION;
metadata: {
organizationId: string;
organizationName: string;
rootOrganizationId: string;
};
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
@@ -4159,7 +4183,7 @@ interface PamResourceCreateEvent {
type: EventType.PAM_RESOURCE_CREATE;
metadata: {
resourceType: string;
gatewayId: string;
gatewayId?: string;
name: string;
};
}
@@ -4224,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
@@ -4609,4 +4753,19 @@ export type Event =
| AutomatedRenewCertificate
| AutomatedRenewCertificateFailed
| UserLoginEvent
| SelectOrganizationEvent;
| SelectOrganizationEvent
| SelectSubOrganizationEvent
| ApprovalPolicyCreateEvent
| ApprovalPolicyUpdateEvent
| ApprovalPolicyDeleteEvent
| ApprovalPolicyListEvent
| ApprovalPolicyGetEvent
| ApprovalRequestGetEvent
| ApprovalRequestListEvent
| ApprovalRequestCreateEvent
| ApprovalRequestApproveEvent
| ApprovalRequestRejectEvent
| ApprovalRequestCancelEvent
| ApprovalRequestGrantListEvent
| ApprovalRequestGrantGetEvent
| ApprovalRequestGrantRevokeEvent;

View File

@@ -350,6 +350,7 @@ export const licenseServiceFactory = ({
actor,
actorId,
actorOrgId,
rootOrgId,
actorAuthMethod,
projectId,
refreshCache
@@ -360,12 +361,12 @@ export const licenseServiceFactory = ({
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
scope: OrganizationActionScope.Any
});
if (refreshCache) {
await refreshPlan(orgId);
await refreshPlan(rootOrgId);
}
const plan = await getPlan(orgId, projectId);
const plan = await getPlan(rootOrgId, projectId);
return plan;
};

View File

@@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = {
export type TOrgPlanDTO = {
projectId?: string;
refreshCache?: boolean;
rootOrgId: string;
} & TOrgPermission;
export type TStartOrgTrialDTO = {

View File

@@ -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
})

View File

@@ -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";
@@ -52,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>;
@@ -68,7 +89,10 @@ export const pamAccountServiceFactory = ({
licenseService,
kmsService,
gatewayV2Service,
auditLogService
auditLogService,
approvalPolicyDAL,
approvalRequestGrantsDAL,
pamSessionExpirationService
}: TPamAccountServiceFactoryDep) => {
const create = async (
{
@@ -136,7 +160,8 @@ export const pamAccountServiceFactory = ({
resource.resourceType as PamResource,
connectionDetails,
resource.gatewayId,
gatewayV2Service
gatewayV2Service,
resource.projectId
);
const validatedCredentials = await factory.validateAccountCredentials(credentials);
@@ -251,7 +276,8 @@ export const pamAccountServiceFactory = ({
resource.resourceType as PamResource,
connectionDetails,
resource.gatewayId,
gatewayV2Service
gatewayV2Service,
account.projectId
);
const decryptedCredentials = await decryptAccountCredentials({
@@ -280,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) => {
@@ -429,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;
@@ -532,24 +568,108 @@ export const pamAccountServiceFactory = ({
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,
actionProjectType: ActionProjectType.PAM
});
const fac = APPROVAL_POLICY_FACTORY_MAP[ApprovalPolicyType.PamAccess](ApprovalPolicyType.PamAccess);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPamAccountActions.Access,
subject(ProjectPermissionSub.PamAccounts, {
resourceName: resource.name,
accountName: account.name,
accountPath: folderPath
})
const inputs = {
resourceId: resource.id,
accountPath: path.join(folderPath, account.name)
};
const canAccess = await fac.canAccess(approvalRequestGrantsDAL, resource.projectId, actor.id, inputs);
// 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,
account.projectId,
kmsService
);
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,
@@ -565,10 +685,9 @@ export const pamAccountServiceFactory = ({
expiresAt: new Date(Date.now() + duration)
});
const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, projectId, kmsService);
const user = await userDAL.findById(actor.id);
if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` });
if (!gatewayId) {
throw new BadRequestError({ message: "Gateway ID is required for this resource type" });
}
const { host, port } =
resourceType !== PamResource.Kubernetes
@@ -616,11 +735,11 @@ export const pamAccountServiceFactory = ({
projectId
})) as TSqlResourceConnectionDetails;
const credentials = await decryptAccountCredentials({
const credentials = (await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
kmsService,
projectId
});
})) as TSqlAccountCredentials;
metadata = {
username: (credentials as TSqlAccountCredentials).username,
@@ -632,11 +751,11 @@ export const pamAccountServiceFactory = ({
break;
case PamResource.SSH:
{
const credentials = await decryptAccountCredentials({
const credentials = (await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
kmsService,
projectId
});
})) as TSSHAccountCredentials;
metadata = {
username: (credentials as TSSHAccountCredentials).username
@@ -716,7 +835,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"
});
@@ -780,7 +899,8 @@ export const pamAccountServiceFactory = ({
resourceType as PamResource,
connectionDetails,
gatewayId,
gatewayV2Service
gatewayV2Service,
account.projectId
);
const newCredentials = await factory.rotateAccountCredentials(

View File

@@ -6,8 +6,10 @@ 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;

View File

@@ -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)
};
};

View File

@@ -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
};
};

View File

@@ -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];
};

View File

@@ -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
})
});

View File

@@ -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>;

View 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";

View File

@@ -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()
});

View File

@@ -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"))

View File

@@ -2,7 +2,8 @@ export enum PamResource {
Postgres = "postgres",
MySQL = "mysql",
SSH = "ssh",
Kubernetes = "kubernetes"
Kubernetes = "kubernetes",
AwsIam = "aws-iam"
}
export enum PamResourceOrderBy {

View File

@@ -1,6 +1,7 @@
import { awsIamResourceFactory } from "./aws-iam/aws-iam-resource-factory";
import { kubernetesResourceFactory } from "./kubernetes/kubernetes-resource-factory";
import { PamResource } from "./pam-resource-enums";
import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types";
import { kubernetesResourceFactory } from "./kubernetes/kubernetes-resource-factory";
import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
import { sshResourceFactory } from "./ssh/ssh-resource-factory";
@@ -10,5 +11,6 @@ export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryIm
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation,
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation,
[PamResource.Kubernetes]: kubernetesResourceFactory as TPamResourceFactoryImplementation
[PamResource.Kubernetes]: kubernetesResourceFactory as TPamResourceFactoryImplementation,
[PamResource.AwsIam]: awsIamResourceFactory as TPamResourceFactoryImplementation
};

View File

@@ -3,15 +3,19 @@ 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 { getKubernetesResourceListItem } from "./kubernetes/kubernetes-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(), getKubernetesResourceListItem()].sort((a, b) =>
a.name.localeCompare(b.name)
);
return [
getPostgresResourceListItem(),
getMySQLResourceListItem(),
getAwsIamResourceListItem(),
getKubernetesResourceListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
// Resource

View File

@@ -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

View File

@@ -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 };

View File

@@ -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 {
TKubernetesAccount,
TKubernetesAccountCredentials,
@@ -28,27 +34,30 @@ import {
} from "./ssh/ssh-resource-types";
// Resource types
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TKubernetesResource;
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TAwsIamResource | TKubernetesResource;
export type TPamResourceConnectionDetails =
| TPostgresResourceConnectionDetails
| TMySQLResourceConnectionDetails
| TSSHResourceConnectionDetails
| TKubernetesResourceConnectionDetails;
| TKubernetesResourceConnectionDetails
| TAwsIamResourceConnectionDetails;
// Account types
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TKubernetesAccount;
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TAwsIamAccount | TKubernetesAccount;
export type TPamAccountCredentials =
| TPostgresAccountCredentials
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
| TMySQLAccountCredentials
| TSSHAccountCredentials
| TKubernetesAccountCredentials;
| TKubernetesAccountCredentials
| 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;
@@ -76,8 +85,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>;

View File

@@ -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()
});

View File

@@ -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(
{

View File

@@ -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();

View File

@@ -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()
});

View File

@@ -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 };
};

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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."
)
})
];

View File

@@ -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";

View File

@@ -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"
}
}
};

View File

@@ -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
};
};

View File

@@ -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
});

View File

@@ -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
>;

View File

@@ -214,7 +214,10 @@ export const secretRotationV2DALFactory = (
tx?: Knex
) => {
try {
const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options })
const { limit, offset = 0, sort, ...queryOptions } = options || {};
const baseOptions = { ...queryOptions };
const subquery = baseSecretRotationV2Query({ filter, db, tx, options: baseOptions })
.join(
TableName.SecretRotationV2SecretMapping,
`${TableName.SecretRotationV2SecretMapping}.rotationId`,
@@ -233,6 +236,7 @@ export const secretRotationV2DALFactory = (
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(
selectAllTableCols(TableName.SecretRotationV2),
db.ref("id").withSchema(TableName.SecretV2).as("secretId"),
db.ref("key").withSchema(TableName.SecretV2).as("secretKey"),
db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"),
@@ -252,18 +256,31 @@ export const secretRotationV2DALFactory = (
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue"),
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.SecretRotationV2}."createdAt" DESC) as rank`)
);
if (search) {
void extendedQuery.where((query) => {
void query
void subquery.where((qb) => {
void qb
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
});
}
const secretRotations = await extendedQuery;
let secretRotations: Awaited<typeof subquery>;
if (limit !== undefined) {
const rankOffset = offset + 1;
const queryWithLimit = (tx || db)
.with("inner", subquery)
.select("*")
.from("inner")
.where("inner.rank", ">=", rankOffset)
.andWhere("inner.rank", "<", rankOffset + limit);
secretRotations = (await queryWithLimit) as unknown as Awaited<typeof subquery>;
} else {
secretRotations = await subquery;
}
if (!secretRotations.length) return [];

View File

@@ -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 {

View File

@@ -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 = () => {

View File

@@ -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
};

View File

@@ -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 = ({

View File

@@ -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;

View File

@@ -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
]);

View File

@@ -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 = [

View File

@@ -170,10 +170,13 @@ export const IDENTITIES = {
}
} as const;
const IDENTITY_AUTH_SUB_ORGANIZATION_NAME = "sub-organization name to scope the token to";
export const UNIVERSAL_AUTH = {
LOGIN: {
clientId: "Your Machine Identity Client ID.",
clientSecret: "Your Machine Identity Client Secret."
clientSecret: "Your Machine Identity Client Secret.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -247,7 +250,8 @@ export const LDAP_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login.",
username: "The username of the LDAP user to login.",
password: "The password of the LDAP user to login."
password: "The password of the LDAP user to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.",
@@ -312,7 +316,8 @@ export const ALICLOUD_AUTH = {
Timestamp: "The timestamp of the request in UTC, formatted as 'YYYY-MM-DDTHH:mm:ssZ'.",
SignatureVersion: "The signature version. For STS GetCallerIdentity, this should be '1.0'.",
SignatureNonce: "A unique random string to prevent replay attacks.",
Signature: "The signature string calculated based on the request parameters and AccessKey Secret."
Signature: "The signature string calculated based on the request parameters and AccessKey Secret.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -340,7 +345,8 @@ export const ALICLOUD_AUTH = {
export const TLS_CERT_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login."
identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -378,7 +384,8 @@ export const AWS_AUTH = {
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.",
iamRequestBody:
"The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.",
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -416,7 +423,8 @@ export const OCI_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login.",
userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request."
headers: "The headers of the signed request.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -448,7 +456,8 @@ export const OCI_AUTH = {
export const AZURE_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login."
identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -482,7 +491,8 @@ export const AZURE_AUTH = {
export const GCP_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login."
identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -520,7 +530,8 @@ export const GCP_AUTH = {
export const KUBERNETES_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login."
identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -600,7 +611,8 @@ export const TOKEN_AUTH = {
},
CREATE_TOKEN: {
identityId: "The ID of the machine identity to create the token for.",
name: "The name of the token to create."
name: "The name of the token to create.",
subOrganizationName: "The sub organization name to scope the token to."
},
UPDATE_TOKEN: {
tokenId: "The ID of the token to update metadata for.",
@@ -613,7 +625,8 @@ export const TOKEN_AUTH = {
export const OIDC_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login."
identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -653,7 +666,8 @@ export const OIDC_AUTH = {
export const JWT_AUTH = {
LOGIN: {
identityId: "The ID of the machine identity to login."
identityId: "The ID of the machine identity to login.",
subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME
},
ATTACH: {
identityId: "The ID of the machine identity to attach the configuration onto.",
@@ -2860,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: {
@@ -2890,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."
}
}
};

View File

@@ -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 ----------------------------------------------------------------------------- */

View File

@@ -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;
}
}

View File

@@ -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 };

View File

@@ -8,7 +8,6 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { slugSchema } from "@app/server/lib/schemas";
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -152,15 +151,10 @@ export const injectIdentity = fp(
if (!authMode) return;
const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined;
if (subOrganizationSelector) {
await slugSchema().parseAsync(subOrganizationSelector);
}
switch (authMode) {
case AuthMode.JWT: {
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
await server.services.authToken.fnValidateJwtIdentity(token);
requestContext.set("orgId", orgId);
requestContext.set("orgName", orgName);
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
@@ -180,11 +174,7 @@ export const injectIdentity = fp(
break;
}
case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(
token,
req.realIp,
subOrganizationSelector
);
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId);
requestContext.set("orgName", identity.orgName);
@@ -223,9 +213,6 @@ export const injectIdentity = fp(
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
requestContext.set("orgId", serviceToken.orgId);
if (subOrganizationSelector)
throw new BadRequestError({ message: `Service token doesn't support sub organization selector` });
req.auth = {
orgId: serviceToken.orgId,
rootOrgId: serviceToken.rootOrgId,
@@ -248,9 +235,6 @@ export const injectIdentity = fp(
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
requestContext.set("orgId", orgId);
if (subOrganizationSelector)
throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` });
req.auth = {
authMode: AuthMode.SCIM_TOKEN,
actor,

View File

@@ -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,

View File

@@ -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[] = [];

View File

@@ -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()
});

View File

@@ -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

View File

@@ -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
};

View File

@@ -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
});
};

View File

@@ -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 };
}
});
};

View File

@@ -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
});
}
};

View File

@@ -81,7 +81,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
response: {
200: z.object({
token: z.string(),
organizationId: z.string().optional()
organizationId: z.string().optional(),
subOrganizationId: z.string().optional()
})
}
},
@@ -89,14 +90,15 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
if (decodedToken.organizationId) {
const org = await server.services.org.findOrganizationById(
decodedToken.userId,
decodedToken.organizationId,
decodedToken.authMethod,
decodedToken.organizationId,
decodedToken.organizationId
);
const org = await server.services.org.findOrganizationById({
userId: decodedToken.userId,
orgId: decodedToken.subOrganizationId ? decodedToken.subOrganizationId : decodedToken.organizationId,
actorAuthMethod: decodedToken.authMethod,
actorOrgId: decodedToken.subOrganizationId ? decodedToken.subOrganizationId : decodedToken.organizationId,
rootOrgId: decodedToken.organizationId
});
if (org && org.userTokenExpiration) {
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
}
@@ -110,14 +112,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
tokenVersionId: tokenVersion.id,
accessVersion: tokenVersion.accessVersion,
organizationId: decodedToken.organizationId,
...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }),
isMfaVerified: decodedToken.isMfaVerified,
mfaMethod: decodedToken.mfaMethod
},
appCfg.AUTH_SECRET,
{ expiresIn }
);
return { token, organizationId: decodedToken.organizationId };
return { token, organizationId: decodedToken.organizationId, subOrganizationId: decodedToken.subOrganizationId };
}
});
};

View File

@@ -316,13 +316,11 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
params: z.object({
requestId: z.string().uuid()
}),
query: z.object({
projectId: z.string().uuid()
}),
response: {
200: z.object({
status: z.nativeEnum(CertificateRequestStatus),
certificate: z.string().nullable(),
certificateId: z.string().nullable(),
privateKey: z.string().nullable(),
serialNumber: z.string().nullable(),
errorMessage: z.string().nullable(),
@@ -333,18 +331,17 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.certificateRequest.getCertificateFromRequest({
const { certificateRequest, projectId } = await server.services.certificateRequest.getCertificateFromRequest({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: (req.query as { projectId: string }).projectId,
certificateRequestId: req.params.requestId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: (req.query as { projectId: string }).projectId,
projectId,
event: {
type: EventType.GET_CERTIFICATE_REQUEST,
metadata: {
@@ -352,7 +349,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
}
}
});
return data;
return certificateRequest;
}
});

View File

@@ -624,7 +624,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional()
tags: SanitizedTagSchema.array().optional(),
reminder: RemindersSchema.extend({
recipients: z.string().array()
}).nullable()
})
.nullable()
.array()
@@ -743,6 +746,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
>[number]["secrets"][number] & {
isEmpty: boolean;
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
}
> | null)[];
})[]
@@ -847,27 +851,38 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
);
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = (
await server.services.secretRotationV2.getDashboardSecretRotations(
{
projectId,
search,
orderBy,
orderDirection,
environments: [environment],
secretPath,
limit: remainingLimit,
offset: adjustedOffset
},
req.permission
)
).map((rotation) => ({
const rawSecretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
{
projectId,
search,
orderBy,
orderDirection,
environments: [environment],
secretPath,
limit: remainingLimit,
offset: adjustedOffset
},
req.permission
);
const allRotationSecretIds = rawSecretRotations
.flatMap((rotation) => rotation.secrets)
.filter((secret) => Boolean(secret))
.map((secret) => secret.id);
const rotationReminders =
allRotationSecretIds.length > 0
? await server.services.reminder.getRemindersForDashboard(allRotationSecretIds)
: {};
secretRotations = rawSecretRotations.map((rotation) => ({
...rotation,
secrets: rotation.secrets.map((secret) =>
secret
? {
...secret,
isEmpty: !secret.secretValue
isEmpty: !secret.secretValue,
reminder: rotationReminders[secret.id] ?? null
}
: secret
)
@@ -948,7 +963,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search,
tagSlugs: tags,
includeTagsInSearch: true,
includeMetadataInSearch: true
includeMetadataInSearch: true,
excludeRotatedSecrets: includeSecretRotations
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
@@ -970,7 +986,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
offset: adjustedOffset,
tagSlugs: tags,
includeTagsInSearch: true,
includeMetadataInSearch: true
includeMetadataInSearch: true,
excludeRotatedSecrets: includeSecretRotations
})
).secrets;

View File

@@ -5,6 +5,7 @@ import { IdentityAlicloudAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ALICLOUD_AUTH, ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -38,6 +39,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
message: "AccessKeyId must be alphanumeric"
})
.describe(ALICLOUD_AUTH.LOGIN.AccessKeyId),
subOrganizationName: slugSchema().optional().describe(ALICLOUD_AUTH.LOGIN.subOrganizationName),
SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod),
Timestamp: z
.string()

View File

@@ -4,6 +4,7 @@ import { IdentityAwsAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, AWS_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -28,7 +29,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId),
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody),
iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders)
iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders),
subOrganizationName: slugSchema().optional().describe(AWS_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({

View File

@@ -4,6 +4,7 @@ import { IdentityAzureAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, AZURE_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -23,7 +24,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
description: "Login with Azure Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId),
jwt: z.string()
jwt: z.string(),
subOrganizationName: slugSchema().optional().describe(AZURE_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({

View File

@@ -4,6 +4,7 @@ import { IdentityGcpAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, GCP_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -23,7 +24,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
description: "Login with GCP Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId),
jwt: z.string()
jwt: z.string(),
subOrganizationName: slugSchema().optional().describe(GCP_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({

View File

@@ -4,6 +4,7 @@ import { IdentityJwtAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, JWT_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -99,7 +100,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
description: "Login with JWT Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId),
jwt: z.string().trim()
jwt: z.string().trim(),
subOrganizationName: slugSchema().optional().describe(JWT_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({
@@ -112,10 +114,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
},
handler: async (req) => {
const { identityJwtAuth, accessToken, identityAccessToken, identity } =
await server.services.identityJwtAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.identityJwtAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -44,7 +45,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
description: "Login with Kubernetes Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId),
jwt: z.string().trim()
jwt: z.string().trim(),
subOrganizationName: slugSchema().optional().describe(KUBERNETES_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({
@@ -57,10 +59,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
},
handler: async (req) => {
const { identityKubernetesAuth, accessToken, identityAccessToken, identity } =
await server.services.identityKubernetesAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.identityKubernetesAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -21,6 +21,7 @@ import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -124,7 +125,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
body: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId),
username: z.string().describe(LDAP_AUTH.LOGIN.username),
password: z.string().describe(LDAP_AUTH.LOGIN.password)
password: z.string().describe(LDAP_AUTH.LOGIN.password),
subOrganizationName: slugSchema().optional().describe(LDAP_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({
@@ -163,7 +165,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
const { identityId, user } = req.passportMachineIdentity;
const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({
identityId
identityId,
subOrganizationName: req.body.subOrganizationName
});
await server.services.auditLog.createAuditLog({

View File

@@ -4,6 +4,7 @@ import { IdentityOciAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -40,7 +41,8 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
});
}
})
.describe(OCI_AUTH.LOGIN.headers)
.describe(OCI_AUTH.LOGIN.headers),
subOrganizationName: slugSchema().optional().describe(OCI_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({

View File

@@ -4,6 +4,7 @@ import { IdentityOidcAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OIDC_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -47,7 +48,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
description: "Login with OIDC Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId),
jwt: z.string().trim()
jwt: z.string().trim(),
subOrganizationName: slugSchema().optional().describe(OIDC_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({
@@ -60,10 +62,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
},
handler: async (req) => {
const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } =
await server.services.identityOidcAuth.login({
identityId: req.body.identityId,
jwt: req.body.jwt
});
await server.services.identityOidcAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
@@ -46,7 +47,8 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
tags: [ApiDocsTags.TlsCertAuth],
description: "Login with TLS Certificate Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId)
identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId),
subOrganizationName: slugSchema().optional().describe(TLS_CERT_AUTH.LOGIN.subOrganizationName)
}),
response: {
200: z.object({
@@ -66,7 +68,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
const { identityTlsCertAuth, accessToken, identityAccessToken, identity } =
await server.services.identityTlsCertAuth.login({
identityId: req.body.identityId,
...req.body,
clientCertificate: clientCertificate as string
});

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