mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 17:27:16 +00:00
Merge branch 'main' into fix/UISecretEditPermissionButNotReadValuePermission
This commit is contained in:
Vendored
+3
@@ -5,6 +5,7 @@ import { Redis } from "ioredis";
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { TAssumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||
@@ -109,6 +110,7 @@ declare module "@fastify/request-context" {
|
||||
};
|
||||
};
|
||||
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +243,7 @@ declare module "fastify" {
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+10
@@ -423,6 +423,11 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TSecretReminderRecipients,
|
||||
TSecretReminderRecipientsInsert,
|
||||
TSecretReminderRecipientsUpdate
|
||||
} from "@app/db/schemas/secret-reminder-recipients";
|
||||
|
||||
declare module "knex" {
|
||||
namespace Knex {
|
||||
@@ -994,5 +999,10 @@ declare module "knex/types/tables" {
|
||||
TSecretRotationV2SecretMappingsInsert,
|
||||
TSecretRotationV2SecretMappingsUpdate
|
||||
>;
|
||||
[TableName.SecretReminderRecipients]: KnexOriginal.CompositeTableType<
|
||||
TSecretReminderRecipients,
|
||||
TSecretReminderRecipientsInsert,
|
||||
TSecretReminderRecipientsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasSecretReminderRecipientsTable = await knex.schema.hasTable(TableName.SecretReminderRecipients);
|
||||
|
||||
if (!hasSecretReminderRecipientsTable) {
|
||||
await knex.schema.createTable(TableName.SecretReminderRecipients, (table) => {
|
||||
table.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
table.timestamps(true, true, true);
|
||||
table.uuid("secretId").notNullable();
|
||||
table.uuid("userId").notNullable();
|
||||
table.string("projectId").notNullable();
|
||||
|
||||
// Based on userId rather than project membership ID so we can easily extend group support in the future if need be.
|
||||
// This does however mean we need to manually clean up once a user is removed from a project.
|
||||
table.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
table.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
|
||||
table.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
table.index("secretId");
|
||||
table.unique(["secretId", "userId"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasSecretReminderRecipientsTable = await knex.schema.hasTable(TableName.SecretReminderRecipients);
|
||||
|
||||
if (hasSecretReminderRecipientsTable) {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretReminderRecipients);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectType, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasDefaultUserCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultUserSshCaId");
|
||||
const hasDefaultHostCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultHostSshCaId");
|
||||
|
||||
if (hasDefaultUserCaCol && hasDefaultHostCaCol) {
|
||||
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
|
||||
t.dropForeign(["defaultUserSshCaId"]);
|
||||
t.dropForeign(["defaultHostSshCaId"]);
|
||||
});
|
||||
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
|
||||
// allow nullable (does not wipe existing values)
|
||||
t.uuid("defaultUserSshCaId").nullable().alter();
|
||||
t.uuid("defaultHostSshCaId").nullable().alter();
|
||||
// re-add with SET NULL behavior (previously CASCADE)
|
||||
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
|
||||
// (dangtony98): backfill by adding null defaults CAs for all existing Infisical SSH projects
|
||||
// that do not have an associated ProjectSshConfig record introduced in Infisical SSH V2.
|
||||
|
||||
const allProjects = await knex(TableName.Project).where("type", ProjectType.SSH).select("id");
|
||||
|
||||
const projectsWithConfig = await knex(TableName.ProjectSshConfig).select("projectId");
|
||||
const projectIdsWithConfig = new Set(projectsWithConfig.map((config) => config.projectId));
|
||||
|
||||
const projectsNeedingConfig = allProjects.filter((project) => !projectIdsWithConfig.has(project.id));
|
||||
|
||||
if (projectsNeedingConfig.length > 0) {
|
||||
const configsToInsert = projectsNeedingConfig.map((project) => ({
|
||||
projectId: project.id,
|
||||
defaultUserSshCaId: null,
|
||||
defaultHostSshCaId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}));
|
||||
|
||||
await knex.batchInsert(TableName.ProjectSshConfig, configsToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAliasColumn = await knex.schema.hasColumn(TableName.SshHost, "alias");
|
||||
if (!hasAliasColumn) {
|
||||
await knex.schema.alterTable(TableName.SshHost, (t) => {
|
||||
t.string("alias").nullable();
|
||||
t.unique(["projectId", "alias"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAliasColumn = await knex.schema.hasColumn(TableName.SshHost, "alias");
|
||||
if (hasAliasColumn) {
|
||||
await knex.schema.alterTable(TableName.SshHost, (t) => {
|
||||
t.dropUnique(["projectId", "alias"]);
|
||||
t.dropColumn("alias");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export const CertificatesSchema = z.object({
|
||||
notAfter: z.date(),
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().default("").nullable().optional(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
caCertId: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const KmipOrgServerCertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
commonName: z.string(),
|
||||
altNames: z.string(),
|
||||
altNames: z.string().nullable().optional(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
|
||||
@@ -146,7 +146,8 @@ export enum TableName {
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
KmipClientCertificates = "kmip_client_certificates",
|
||||
SecretRotationV2 = "secret_rotations_v2",
|
||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings"
|
||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
||||
SecretReminderRecipients = "secret_reminder_recipients"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
||||
@@ -30,9 +30,9 @@ export const OidcConfigsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
orgId: z.string().uuid(),
|
||||
lastUsed: z.date().nullable().optional(),
|
||||
manageGroupMemberships: z.boolean().default(false),
|
||||
encryptedOidcClientId: zodBuffer,
|
||||
encryptedOidcClientSecret: zodBuffer,
|
||||
manageGroupMemberships: z.boolean().default(false),
|
||||
jwtSignatureAlgorithm: z.string().default("RS256")
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const OrganizationsSchema = z.object({
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
secretShareSendToAnyone: z.boolean().default(true).nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
|
||||
@@ -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 SecretReminderRecipientsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
secretId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
projectId: z.string()
|
||||
});
|
||||
|
||||
export type TSecretReminderRecipients = z.infer<typeof SecretReminderRecipientsSchema>;
|
||||
export type TSecretReminderRecipientsInsert = Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>;
|
||||
export type TSecretReminderRecipientsUpdate = Partial<
|
||||
Omit<z.input<typeof SecretReminderRecipientsSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -16,7 +16,8 @@ export const SshHostsSchema = z.object({
|
||||
userCertTtl: z.string(),
|
||||
hostCertTtl: z.string(),
|
||||
userSshCaId: z.string().uuid(),
|
||||
hostSshCaId: z.string().uuid()
|
||||
hostSshCaId: z.string().uuid(),
|
||||
alias: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSshHosts = z.infer<typeof SshHostsSchema>;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAssumePrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:projectId/assume-privileges",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
actorType: z.enum([ActorType.USER, ActorType.IDENTITY]),
|
||||
actorId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req, res) => {
|
||||
if (req.auth.authMode === AuthMode.JWT) {
|
||||
const payload = await server.services.assumePrivileges.assumeProjectPrivileges({
|
||||
targetActorType: req.body.actorType,
|
||||
targetActorId: req.body.actorId,
|
||||
projectId: req.params.projectId,
|
||||
actorPermissionDetails: req.permission,
|
||||
tokenVersionId: req.auth.tokenVersionId
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
void res.setCookie("infisical-project-assume-privileges", payload.assumePrivilegesToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED,
|
||||
maxAge: 3600 // 1 hour in seconds
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START,
|
||||
metadata: {
|
||||
projectId: req.params.projectId,
|
||||
requesterEmail: req.auth.user.username,
|
||||
requesterId: req.auth.user.id,
|
||||
targetActorType: req.body.actorType,
|
||||
targetActorId: req.body.actorId,
|
||||
duration: "1hr"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { message: "Successfully assumed role" };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid auth mode" });
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:projectId/assume-privileges",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req, res) => {
|
||||
const assumedPrivilegeDetails = requestContext.get("assumedPrivilegeDetails");
|
||||
if (req.auth.authMode === AuthMode.JWT && assumedPrivilegeDetails) {
|
||||
const appCfg = getConfig();
|
||||
void res.setCookie("infisical-project-assume-privileges", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED,
|
||||
expires: new Date(0)
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END,
|
||||
metadata: {
|
||||
projectId: req.params.projectId,
|
||||
requesterEmail: req.auth.user.username,
|
||||
requesterId: req.auth.user.id,
|
||||
targetActorId: assumedPrivilegeDetails.actorId,
|
||||
targetActorType: assumedPrivilegeDetails.actorType
|
||||
}
|
||||
}
|
||||
});
|
||||
return { message: "Successfully exited assumed role" };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid auth mode" });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-templat
|
||||
|
||||
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
|
||||
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
|
||||
import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
@@ -45,6 +46,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await projectRouter.register(registerProjectRoleRouter);
|
||||
await projectRouter.register(registerProjectRouter);
|
||||
await projectRouter.register(registerTrustedIpRouter);
|
||||
await projectRouter.register(registerAssumePrivilegeRouter);
|
||||
},
|
||||
{ prefix: "/workspace" }
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import {
|
||||
backfillPermissionV1SchemaToV2Schema,
|
||||
ProjectPermissionV1Schema
|
||||
@@ -245,13 +245,22 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
data: z.object({
|
||||
membership: ProjectMembershipsSchema.extend({
|
||||
membership: z.object({
|
||||
id: z.string(),
|
||||
roles: z
|
||||
.object({
|
||||
role: z.string()
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
assumedPrivilegeDetails: z
|
||||
.object({
|
||||
actorId: z.string(),
|
||||
actorType: z.string(),
|
||||
actorName: z.string(),
|
||||
actorEmail: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
permissions: z.any().array()
|
||||
})
|
||||
})
|
||||
@@ -259,14 +268,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { permissions, membership } = await server.services.projectRole.getUserPermission(
|
||||
const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission(
|
||||
req.permission.id,
|
||||
req.params.projectId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
return { data: { permissions, membership } };
|
||||
return {
|
||||
data: {
|
||||
permissions,
|
||||
membership,
|
||||
assumedPrivilegeDetails
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
||||
import { SSH_HOSTS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -96,10 +97,12 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
hostname: z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.refine((v) => isValidHostname(v), {
|
||||
message: "Hostname must be a valid hostname"
|
||||
})
|
||||
.describe(SSH_HOSTS.CREATE.hostname),
|
||||
alias: slugSchema({ min: 0, max: 64, field: "alias" }).describe(SSH_HOSTS.CREATE.alias).default(""),
|
||||
userCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
@@ -138,6 +141,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname,
|
||||
alias: host.alias ?? null,
|
||||
userCertTtl: host.userCertTtl,
|
||||
hostCertTtl: host.hostCertTtl,
|
||||
loginMappings: host.loginMappings,
|
||||
@@ -166,12 +170,14 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
hostname: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine((v) => isValidHostname(v), {
|
||||
message: "Hostname must be a valid hostname"
|
||||
})
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.hostname),
|
||||
alias: slugSchema({ min: 0, max: 64, field: "alias" }).describe(SSH_HOSTS.UPDATE.alias).optional(),
|
||||
userCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
@@ -208,6 +214,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname,
|
||||
alias: host.alias,
|
||||
userCertTtl: host.userCertTtl,
|
||||
hostCertTtl: host.hostCertTtl,
|
||||
loginMappings: host.loginMappings,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
AwsIamUserSecretRotationGeneratedCredentialsSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
CreateAwsIamUserSecretRotationSchema,
|
||||
UpdateAwsIamUserSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerAwsIamUserSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.AwsIamUserSecret,
|
||||
server,
|
||||
responseSchema: AwsIamUserSecretRotationSchema,
|
||||
createSchema: CreateAwsIamUserSecretRotationSchema,
|
||||
updateSchema: UpdateAwsIamUserSecretRotationSchema,
|
||||
generatedCredentialsSchema: AwsIamUserSecretRotationGeneratedCredentialsSchema
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
@@ -12,5 +14,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
> = {
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
|
||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CreateLdapPasswordRotationSchema,
|
||||
LdapPasswordRotationGeneratedCredentialsSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
UpdateLdapPasswordRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.LdapPassword,
|
||||
server,
|
||||
responseSchema: LdapPasswordRotationSchema,
|
||||
createSchema: CreateLdapPasswordRotationSchema,
|
||||
updateSchema: UpdateLdapPasswordRotationSchema,
|
||||
generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
@@ -13,7 +15,9 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
MsSqlCredentialsRotationListItemSchema,
|
||||
Auth0ClientSecretRotationListItemSchema
|
||||
Auth0ClientSecretRotationListItemSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { TAssumeProjectPrivilegeDTO } from "./assume-privilege-types";
|
||||
|
||||
type TAssumePrivilegeServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TAssumePrivilegeServiceFactory = ReturnType<typeof assumePrivilegeServiceFactory>;
|
||||
|
||||
export const assumePrivilegeServiceFactory = ({ projectDAL, permissionService }: TAssumePrivilegeServiceFactoryDep) => {
|
||||
const assumeProjectPrivileges = async ({
|
||||
targetActorType,
|
||||
targetActorId,
|
||||
projectId,
|
||||
actorPermissionDetails,
|
||||
tokenVersionId
|
||||
}: TAssumeProjectPrivilegeDTO) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actorPermissionDetails.type,
|
||||
actorId: actorPermissionDetails.id,
|
||||
projectId,
|
||||
actorAuthMethod: actorPermissionDetails.authMethod,
|
||||
actorOrgId: actorPermissionDetails.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
if (targetActorType === ActorType.USER) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionMemberActions.AssumePrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.AssumePrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
}
|
||||
|
||||
// check entity is part of project
|
||||
await permissionService.getProjectPermission({
|
||||
actor: targetActorType,
|
||||
actorId: targetActorId,
|
||||
projectId,
|
||||
actorAuthMethod: actorPermissionDetails.authMethod,
|
||||
actorOrgId: actorPermissionDetails.orgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const assumePrivilegesToken = jwt.sign(
|
||||
{
|
||||
tokenVersionId,
|
||||
actorType: targetActorType,
|
||||
actorId: targetActorId,
|
||||
projectId,
|
||||
requesterId: actorPermissionDetails.id
|
||||
},
|
||||
appCfg.AUTH_SECRET,
|
||||
{ expiresIn: "1hr" }
|
||||
);
|
||||
|
||||
return { actorType: targetActorType, actorId: targetActorId, projectId, assumePrivilegesToken };
|
||||
};
|
||||
|
||||
const verifyAssumePrivilegeToken = (token: string, tokenVersionId: string) => {
|
||||
const appCfg = getConfig();
|
||||
const decodedToken = jwt.verify(token, appCfg.AUTH_SECRET) as {
|
||||
tokenVersionId: string;
|
||||
projectId: string;
|
||||
requesterId: string;
|
||||
actorType: ActorType;
|
||||
actorId: string;
|
||||
};
|
||||
if (decodedToken.tokenVersionId !== tokenVersionId) {
|
||||
throw new ForbiddenRequestError({ message: "Invalid token version" });
|
||||
}
|
||||
return decodedToken;
|
||||
};
|
||||
|
||||
return {
|
||||
assumeProjectPrivileges,
|
||||
verifyAssumePrivilegeToken
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
export type TAssumeProjectPrivilegeDTO = {
|
||||
targetActorType: ActorType.USER | ActorType.IDENTITY;
|
||||
targetActorId: string;
|
||||
projectId: string;
|
||||
tokenVersionId: string;
|
||||
actorPermissionDetails: OrgServiceActor;
|
||||
};
|
||||
@@ -234,6 +234,7 @@ export enum EventType {
|
||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
|
||||
ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso",
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
@@ -248,6 +249,8 @@ export enum EventType {
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
|
||||
GET_PROJECT_SSH_CONFIG = "get-project-ssh-config",
|
||||
UPDATE_PROJECT_SSH_CONFIG = "update-project-ssh-config",
|
||||
INTEGRATION_SYNCED = "integration-synced",
|
||||
CREATE_CMEK = "create-cmek",
|
||||
UPDATE_CMEK = "update-cmek",
|
||||
@@ -317,7 +320,9 @@ export enum EventType {
|
||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||
SECRET_ROTATION_ROTATE_SECRETS = "secret-rotation-rotate-secrets",
|
||||
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request"
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -1491,6 +1496,7 @@ interface CreateSshHost {
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname: string;
|
||||
alias: string | null;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
@@ -1509,6 +1515,7 @@ interface UpdateSshHost {
|
||||
metadata: {
|
||||
sshHostId: string;
|
||||
hostname?: string;
|
||||
alias?: string | null;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
@@ -1907,6 +1914,11 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface OrgAdminBypassSSOEvent {
|
||||
type: EventType.ORG_ADMIN_BYPASS_SSO;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
@@ -1986,6 +1998,25 @@ interface GetProjectSlackConfig {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectSshConfig {
|
||||
type: EventType.GET_PROJECT_SSH_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectSshConfig {
|
||||
type: EventType.UPDATE_PROJECT_SSH_CONFIG;
|
||||
metadata: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
defaultUserSshCaId?: string | null;
|
||||
defaultHostSshCaId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface IntegrationSyncedEvent {
|
||||
type: EventType.INTEGRATION_SYNCED;
|
||||
metadata: {
|
||||
@@ -2425,6 +2456,29 @@ interface ProjectAccessRequestEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectAssumePrivilegesEvent {
|
||||
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_START;
|
||||
metadata: {
|
||||
projectId: string;
|
||||
requesterId: string;
|
||||
requesterEmail: string;
|
||||
targetActorType: ActorType;
|
||||
targetActorId: string;
|
||||
duration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectAssumePrivilegesExitEvent {
|
||||
type: EventType.PROJECT_ASSUME_PRIVILEGE_SESSION_END;
|
||||
metadata: {
|
||||
projectId: string;
|
||||
requesterId: string;
|
||||
requesterEmail: string;
|
||||
targetActorType: ActorType;
|
||||
targetActorId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SetupKmipEvent {
|
||||
type: EventType.SETUP_KMIP;
|
||||
metadata: {
|
||||
@@ -2656,6 +2710,7 @@ export type Event =
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent
|
||||
| OrgAdminBypassSSOEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
@@ -2670,6 +2725,8 @@ export type Event =
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig
|
||||
| GetProjectSshConfig
|
||||
| UpdateProjectSshConfig
|
||||
| IntegrationSyncedEvent
|
||||
| CreateCmekEvent
|
||||
| UpdateCmekEvent
|
||||
@@ -2727,6 +2784,8 @@ export type Event =
|
||||
| KmipOperationLocateEvent
|
||||
| KmipOperationRegisterEvent
|
||||
| ProjectAccessRequestEvent
|
||||
| ProjectAssumePrivilegesEvent
|
||||
| ProjectAssumePrivilegesExitEvent
|
||||
| CreateSecretRequestEvent
|
||||
| SecretApprovalRequestReview
|
||||
| GetSecretRotationsEvent
|
||||
|
||||
@@ -130,7 +130,17 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (expireAt > maxExpiryDate) throw new BadRequestError({ message: "TTL cannot be larger than max TTL" });
|
||||
}
|
||||
|
||||
const { entityId, data } = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
|
||||
let result;
|
||||
try {
|
||||
result = await selectedProvider.create(decryptedStoredInput, expireAt.getTime());
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||
throw new BadRequestError({ message: error.sqlMessage as string });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const { entityId, data } = result;
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.create({
|
||||
expireAt,
|
||||
version: 1,
|
||||
|
||||
@@ -83,18 +83,26 @@ export const externalKmsServiceFactory = ({
|
||||
throw error;
|
||||
});
|
||||
|
||||
// if missing kms key this generate a new kms key id and returns new provider input
|
||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||
try {
|
||||
// if missing kms key this generate a new kms key id and returns new provider input
|
||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||
|
||||
await externalKms.validateConnection();
|
||||
await externalKms.validateConnection();
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KmsProviders.Gcp:
|
||||
{
|
||||
const externalKms = await GcpKmsProviderFactory({ inputs: provider.inputs });
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(provider.inputs);
|
||||
try {
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(provider.inputs);
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -186,8 +194,12 @@ export const externalKmsServiceFactory = ({
|
||||
);
|
||||
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
try {
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KmsProviders.Gcp:
|
||||
@@ -197,8 +209,12 @@ export const externalKmsServiceFactory = ({
|
||||
);
|
||||
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
|
||||
const externalKms = await GcpKmsProviderFactory({ inputs: updatedProviderInput });
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
try {
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -368,7 +384,11 @@ export const externalKmsServiceFactory = ({
|
||||
|
||||
const fetchGcpKeys = async ({ credential, gcpRegion }: Pick<TExternalKmsGcpSchema, "credential" | "gcpRegion">) => {
|
||||
const externalKms = await GcpKmsProviderFactory({ inputs: { credential, gcpRegion, keyName: "" } });
|
||||
return externalKms.getKeysList();
|
||||
try {
|
||||
return await externalKms.getKeysList();
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -102,10 +102,19 @@ export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Pro
|
||||
return { data: Buffer.from(decryptionCommand.Plaintext) };
|
||||
};
|
||||
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
awsClient.destroy();
|
||||
} catch (error) {
|
||||
throw new Error("Failed to cleanup AWS KMS client", { cause: error });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
generateInputKmsKey,
|
||||
validateConnection,
|
||||
encrypt,
|
||||
decrypt
|
||||
decrypt,
|
||||
cleanup
|
||||
};
|
||||
};
|
||||
|
||||
@@ -45,6 +45,14 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
await gcpKmsClient.close();
|
||||
} catch (error) {
|
||||
throw new Error("Failed to cleanup GCP KMS client", { cause: error });
|
||||
}
|
||||
};
|
||||
|
||||
// Used when adding the KMS to fetch the list of keys in specified region
|
||||
const getKeysList = async () => {
|
||||
try {
|
||||
@@ -108,6 +116,7 @@ export const GcpKmsProviderFactory = async ({ inputs }: GcpKmsProviderArgs): Pro
|
||||
validateConnection,
|
||||
getKeysList,
|
||||
encrypt,
|
||||
decrypt
|
||||
decrypt,
|
||||
cleanup
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,4 +98,5 @@ export type TExternalKmsProviderFns = {
|
||||
validateConnection: () => Promise<boolean>;
|
||||
encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>;
|
||||
decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>;
|
||||
cleanup: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -551,13 +551,26 @@ export const permissionServiceFactory = ({
|
||||
};
|
||||
|
||||
const getProjectPermission = async <T extends ActorType>({
|
||||
actor,
|
||||
actorId,
|
||||
actor: inputActor,
|
||||
actorId: inputActorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType
|
||||
}: TGetProjectPermissionArg): Promise<TProjectPermissionRT<T>> => {
|
||||
let actor = inputActor;
|
||||
let actorId = inputActorId;
|
||||
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
|
||||
if (
|
||||
assumedPrivilegeDetailsCtx &&
|
||||
actor === ActorType.USER &&
|
||||
actorId === assumedPrivilegeDetailsCtx.requesterId &&
|
||||
projectId === assumedPrivilegeDetailsCtx.projectId
|
||||
) {
|
||||
actor = assumedPrivilegeDetailsCtx.actorType;
|
||||
actorId = assumedPrivilegeDetailsCtx.actorId;
|
||||
}
|
||||
|
||||
switch (actor) {
|
||||
case ActorType.USER:
|
||||
return getUserProjectPermission({
|
||||
|
||||
@@ -50,7 +50,8 @@ export enum ProjectPermissionIdentityActions {
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
GrantPrivileges = "grant-privileges",
|
||||
AssumePrivileges = "assume-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionMemberActions {
|
||||
@@ -58,7 +59,8 @@ export enum ProjectPermissionMemberActions {
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
GrantPrivileges = "grant-privileges",
|
||||
AssumePrivileges = "assume-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionGroupActions {
|
||||
@@ -714,7 +716,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionMemberActions.Delete,
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionMemberActions.GrantPrivileges
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionMemberActions.AssumePrivileges
|
||||
],
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
@@ -736,7 +739,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Delete,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionIdentityActions.AssumePrivileges
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
@@ -965,7 +969,6 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
@@ -1031,7 +1034,6 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
|
||||
@@ -33,6 +33,7 @@ export type TApprovalCreateSecretV2Bridge = {
|
||||
secretComment?: string;
|
||||
reminderNote?: string | null;
|
||||
reminderRepeatDays?: number | null;
|
||||
secretReminderRecipients?: string[] | null;
|
||||
skipMultilineEncoding?: boolean;
|
||||
metadata?: Record<string, string>;
|
||||
secretMetadata?: ResourceMetadataDTO;
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
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 AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "AWS IAM User Secret",
|
||||
type: SecretRotation.AwsIamUserSecret,
|
||||
connection: AppConnection.AWS,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
accessKeyId: "AWS_ACCESS_KEY_ID",
|
||||
secretAccessKey: "AWS_SECRET_ACCESS_KEY"
|
||||
}
|
||||
}
|
||||
};
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import {
|
||||
TAwsIamUserSecretRotationGeneratedCredentials,
|
||||
TAwsIamUserSecretRotationWithConnection
|
||||
} from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret/aws-iam-user-secret-rotation-types";
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws";
|
||||
|
||||
const getCreateDate = (key: AWS.IAM.AccessKeyMetadata): number => {
|
||||
return key.CreateDate ? new Date(key.CreateDate).getTime() : 0;
|
||||
};
|
||||
|
||||
export const awsIamUserSecretRotationFactory: TRotationFactory<
|
||||
TAwsIamUserSecretRotationWithConnection,
|
||||
TAwsIamUserSecretRotationGeneratedCredentials
|
||||
> = (secretRotation) => {
|
||||
const {
|
||||
parameters: { region, userName },
|
||||
connection,
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const $rotateClientSecret = async () => {
|
||||
const { credentials } = await getAwsConnectionConfig(connection, region);
|
||||
const iam = new AWS.IAM({ credentials });
|
||||
|
||||
const { AccessKeyMetadata } = await iam.listAccessKeys({ UserName: userName }).promise();
|
||||
|
||||
if (AccessKeyMetadata && AccessKeyMetadata.length > 0) {
|
||||
// Sort keys by creation date (oldest first)
|
||||
const sortedKeys = [...AccessKeyMetadata].sort((a, b) => getCreateDate(a) - getCreateDate(b));
|
||||
|
||||
// If we already have 2 keys, delete the oldest one
|
||||
if (sortedKeys.length >= 2) {
|
||||
const accessId = sortedKeys[0].AccessKeyId || sortedKeys[1].AccessKeyId;
|
||||
if (accessId) {
|
||||
await iam
|
||||
.deleteAccessKey({
|
||||
UserName: userName,
|
||||
AccessKeyId: accessId
|
||||
})
|
||||
.promise();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { AccessKey } = await iam.createAccessKey({ UserName: userName }).promise();
|
||||
|
||||
return {
|
||||
accessKeyId: AccessKey.AccessKeyId,
|
||||
secretAccessKey: AccessKey.SecretAccessKey
|
||||
};
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||
generatedCredentials,
|
||||
callback
|
||||
) => {
|
||||
const { credentials } = await getAwsConnectionConfig(connection, region);
|
||||
const iam = new AWS.IAM({ credentials });
|
||||
|
||||
await Promise.all(
|
||||
generatedCredentials.map((generatedCredential) =>
|
||||
iam
|
||||
.deleteAccessKey({
|
||||
UserName: userName,
|
||||
AccessKeyId: generatedCredential.accessKeyId
|
||||
})
|
||||
.promise()
|
||||
)
|
||||
);
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TAwsIamUserSecretRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateClientSecret();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAwsIamUserSecretRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const secrets = [
|
||||
{
|
||||
key: secretsMapping.accessKeyId,
|
||||
value: generatedCredentials.accessKeyId
|
||||
},
|
||||
{
|
||||
key: secretsMapping.secretAccessKey,
|
||||
value: generatedCredentials.secretAccessKey
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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 { SecretRotations } from "@app/lib/api-docs";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const AwsIamUserSecretRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
accessKeyId: z.string(),
|
||||
secretAccessKey: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const AwsIamUserSecretRotationParametersSchema = z.object({
|
||||
userName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client Name Required")
|
||||
.describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.userName),
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretRotations.PARAMETERS.AWS_IAM_USER_SECRET.region).optional()
|
||||
});
|
||||
|
||||
const AwsIamUserSecretRotationSecretsMappingSchema = z.object({
|
||||
accessKeyId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.accessKeyId),
|
||||
secretAccessKey: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AWS_IAM_USER_SECRET.secretAccessKey)
|
||||
});
|
||||
|
||||
export const AwsIamUserSecretRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
accessKeyId: z.string(),
|
||||
secretAccessKey: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const AwsIamUserSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AwsIamUserSecret).extend({
|
||||
type: z.literal(SecretRotation.AwsIamUserSecret),
|
||||
parameters: AwsIamUserSecretRotationParametersSchema,
|
||||
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateAwsIamUserSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.AwsIamUserSecret
|
||||
).extend({
|
||||
parameters: AwsIamUserSecretRotationParametersSchema,
|
||||
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsIamUserSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.AwsIamUserSecret
|
||||
).extend({
|
||||
parameters: AwsIamUserSecretRotationParametersSchema.optional(),
|
||||
secretsMapping: AwsIamUserSecretRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const AwsIamUserSecretRotationListItemSchema = z.object({
|
||||
name: z.literal("AWS IAM User Secret"),
|
||||
connection: z.literal(AppConnection.AWS),
|
||||
type: z.literal(SecretRotation.AwsIamUserSecret),
|
||||
template: AwsIamUserSecretRotationTemplateSchema
|
||||
});
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||
|
||||
import {
|
||||
AwsIamUserSecretRotationGeneratedCredentialsSchema,
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
CreateAwsIamUserSecretRotationSchema
|
||||
} from "./aws-iam-user-secret-rotation-schemas";
|
||||
|
||||
export type TAwsIamUserSecretRotation = z.infer<typeof AwsIamUserSecretRotationSchema>;
|
||||
|
||||
export type TAwsIamUserSecretRotationInput = z.infer<typeof CreateAwsIamUserSecretRotationSchema>;
|
||||
|
||||
export type TAwsIamUserSecretRotationListItem = z.infer<typeof AwsIamUserSecretRotationListItemSchema>;
|
||||
|
||||
export type TAwsIamUserSecretRotationWithConnection = TAwsIamUserSecretRotation & {
|
||||
connection: TAwsConnection;
|
||||
};
|
||||
|
||||
export type TAwsIamUserSecretRotationGeneratedCredentials = z.infer<
|
||||
typeof AwsIamUserSecretRotationGeneratedCredentialsSchema
|
||||
>;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./aws-iam-user-secret-rotation-constants";
|
||||
export * from "./aws-iam-user-secret-rotation-schemas";
|
||||
export * from "./aws-iam-user-secret-rotation-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./ldap-password-rotation-constants";
|
||||
export * from "./ldap-password-rotation-schemas";
|
||||
export * from "./ldap-password-rotation-types";
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
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 LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "LDAP Password",
|
||||
type: SecretRotation.LdapPassword,
|
||||
connection: AppConnection.LDAP,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
dn: "LDAP_DN",
|
||||
password: "LDAP_PASSWORD"
|
||||
}
|
||||
}
|
||||
};
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import ldap from "ldapjs";
|
||||
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
|
||||
|
||||
import { generatePassword } from "../shared/utils";
|
||||
import {
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationWithConnection
|
||||
} from "./ldap-password-rotation-types";
|
||||
|
||||
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
|
||||
|
||||
export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
TLdapPasswordRotationWithConnection,
|
||||
TLdapPasswordRotationGeneratedCredentials
|
||||
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { dn, passwordRequirements },
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
|
||||
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
|
||||
try {
|
||||
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
|
||||
|
||||
client.unbind();
|
||||
client.destroy();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const $rotatePassword = async () => {
|
||||
const { credentials, orgId } = connection;
|
||||
|
||||
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
|
||||
|
||||
const client = await getLdapConnectionClient(credentials);
|
||||
const isPersonalRotation = credentials.dn === dn;
|
||||
|
||||
const password = generatePassword(passwordRequirements);
|
||||
|
||||
let changes: ldap.Change[] | ldap.Change;
|
||||
|
||||
switch (credentials.provider) {
|
||||
case LdapProvider.ActiveDirectory:
|
||||
{
|
||||
const encodedPassword = getEncodedPassword(password);
|
||||
|
||||
// service account vs personal password rotation require different changes
|
||||
if (isPersonalRotation) {
|
||||
const currentEncodedPassword = getEncodedPassword(credentials.password);
|
||||
|
||||
changes = [
|
||||
new ldap.Change({
|
||||
operation: "delete",
|
||||
modification: {
|
||||
type: "unicodePwd",
|
||||
values: [currentEncodedPassword]
|
||||
}
|
||||
}),
|
||||
new ldap.Change({
|
||||
operation: "add",
|
||||
modification: {
|
||||
type: "unicodePwd",
|
||||
values: [encodedPassword]
|
||||
}
|
||||
})
|
||||
];
|
||||
} else {
|
||||
changes = new ldap.Change({
|
||||
operation: "replace",
|
||||
modification: {
|
||||
type: "unicodePwd",
|
||||
values: [encodedPassword]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.modify(dn, changes, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Password Rotation Failed");
|
||||
reject(new Error(`Provider Modify Error: ${err.message}`));
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
client.unbind();
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
await $verifyCredentials({ dn, password });
|
||||
|
||||
if (isPersonalRotation) {
|
||||
const updatedCredentials: TLdapConnection["credentials"] = {
|
||||
...credentials,
|
||||
password
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
|
||||
}
|
||||
|
||||
return { dn, password };
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
// we just rotate to a new password, essentially revoking old credentials
|
||||
await $rotatePassword();
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TLdapPasswordRotationGeneratedCredentials> = (
|
||||
generatedCredentials
|
||||
) => {
|
||||
const secrets = [
|
||||
{
|
||||
key: secretsMapping.dn,
|
||||
value: generatedCredentials.dn
|
||||
},
|
||||
{
|
||||
key: secretsMapping.password,
|
||||
value: generatedCredentials.password
|
||||
}
|
||||
];
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import RE2 from "re2";
|
||||
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 { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const LdapPasswordRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
dn: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const LdapPasswordRotationParametersSchema = z.object({
|
||||
dn: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||
.min(1, "Distinguished Name (DN) Required")
|
||||
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
|
||||
passwordRequirements: PasswordRequirementsSchema.optional()
|
||||
});
|
||||
|
||||
const LdapPasswordRotationSecretsMappingSchema = z.object({
|
||||
dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn),
|
||||
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password)
|
||||
});
|
||||
|
||||
export const LdapPasswordRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
dn: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
type: z.literal(SecretRotation.LdapPassword),
|
||||
parameters: LdapPasswordRotationParametersSchema,
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
parameters: LdapPasswordRotationParametersSchema,
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
parameters: LdapPasswordRotationParametersSchema.optional(),
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const LdapPasswordRotationListItemSchema = z.object({
|
||||
name: z.literal("LDAP Password"),
|
||||
connection: z.literal(AppConnection.LDAP),
|
||||
type: z.literal(SecretRotation.LdapPassword),
|
||||
template: LdapPasswordRotationTemplateSchema
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TLdapConnection } from "@app/services/app-connection/ldap";
|
||||
|
||||
import {
|
||||
CreateLdapPasswordRotationSchema,
|
||||
LdapPasswordRotationGeneratedCredentialsSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
LdapPasswordRotationSchema
|
||||
} from "./ldap-password-rotation-schemas";
|
||||
|
||||
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
|
||||
|
||||
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
|
||||
|
||||
export type TLdapPasswordRotationListItem = z.infer<typeof LdapPasswordRotationListItemSchema>;
|
||||
|
||||
export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & {
|
||||
connection: TLdapConnection;
|
||||
};
|
||||
|
||||
export type TLdapPasswordRotationGeneratedCredentials = z.infer<typeof LdapPasswordRotationGeneratedCredentialsSchema>;
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-credentials",
|
||||
Auth0ClientSecret = "auth0-client-secret"
|
||||
Auth0ClientSecret = "auth0-client-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
AwsIamUserSecret = "aws-iam-user-secret"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
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 { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
@@ -18,7 +20,9 @@ import {
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
|
||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
|
||||
@@ -3,12 +3,16 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
||||
[SecretRotation.LdapPassword]: "LDAP Password",
|
||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
|
||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
|
||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
calculateNextRotationAt,
|
||||
@@ -77,6 +78,7 @@ import {
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
|
||||
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
|
||||
export type TSecretRotationV2ServiceFactoryDep = {
|
||||
@@ -114,7 +116,9 @@ type TRotationFactoryImplementation = TRotationFactory<
|
||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
|
||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
@@ -449,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
// even though we have a db constraint we want to check before any rotation of credentials is attempted
|
||||
// to prevent creation failure after external credentials have been modified
|
||||
const conflictingRotation = await secretRotationV2DAL.findOne({
|
||||
name: payload.name,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (conflictingRotation)
|
||||
throw new BadRequestError({
|
||||
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
|
||||
});
|
||||
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
|
||||
|
||||
@@ -12,6 +12,20 @@ import {
|
||||
TAuth0ClientSecretRotationListItem,
|
||||
TAuth0ClientSecretRotationWithConnection
|
||||
} from "./auth0-client-secret";
|
||||
import {
|
||||
TAwsIamUserSecretRotation,
|
||||
TAwsIamUserSecretRotationGeneratedCredentials,
|
||||
TAwsIamUserSecretRotationInput,
|
||||
TAwsIamUserSecretRotationListItem,
|
||||
TAwsIamUserSecretRotationWithConnection
|
||||
} from "./aws-iam-user-secret";
|
||||
import {
|
||||
TLdapPasswordRotation,
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationInput,
|
||||
TLdapPasswordRotationListItem,
|
||||
TLdapPasswordRotationWithConnection
|
||||
} from "./ldap-password";
|
||||
import {
|
||||
TMsSqlCredentialsRotation,
|
||||
TMsSqlCredentialsRotationInput,
|
||||
@@ -27,26 +41,39 @@ import {
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
|
||||
export type TSecretRotationV2 =
|
||||
| TPostgresCredentialsRotation
|
||||
| TMsSqlCredentialsRotation
|
||||
| TAuth0ClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
| TMsSqlCredentialsRotationWithConnection
|
||||
| TAuth0ClientSecretRotationWithConnection;
|
||||
| TAuth0ClientSecretRotationWithConnection
|
||||
| TLdapPasswordRotationWithConnection
|
||||
| TAwsIamUserSecretRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
| TAuth0ClientSecretRotationGeneratedCredentials;
|
||||
| TAuth0ClientSecretRotationGeneratedCredentials
|
||||
| TLdapPasswordRotationGeneratedCredentials
|
||||
| TAwsIamUserSecretRotationGeneratedCredentials;
|
||||
|
||||
export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
| TMsSqlCredentialsRotationInput
|
||||
| TAuth0ClientSecretRotationInput;
|
||||
| TAuth0ClientSecretRotationInput
|
||||
| TLdapPasswordRotationInput
|
||||
| TAwsIamUserSecretRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
| TMsSqlCredentialsRotationListItem
|
||||
| TAuth0ClientSecretRotationListItem;
|
||||
| TAuth0ClientSecretRotationListItem
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem;
|
||||
|
||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
|
||||
import { AwsIamUserSecretRotationSchema } from "./aws-iam-user-secret";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
MsSqlCredentialsRotationSchema,
|
||||
Auth0ClientSecretRotationSchema
|
||||
Auth0ClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema
|
||||
]);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./password-requirements-schema";
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
|
||||
export const PasswordRequirementsSchema = z
|
||||
.object({
|
||||
length: z
|
||||
.number()
|
||||
.min(1, "Password length must be a positive number")
|
||||
.max(250, "Password length must be less than 250")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length),
|
||||
required: z.object({
|
||||
digits: z
|
||||
.number()
|
||||
.min(0, "Digit count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits),
|
||||
lowercase: z
|
||||
.number()
|
||||
.min(0, "Lowercase count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase),
|
||||
uppercase: z
|
||||
.number()
|
||||
.min(0, "Uppercase count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase),
|
||||
symbols: z
|
||||
.number()
|
||||
.min(0, "Symbol count must be non-negative")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols)
|
||||
}),
|
||||
allowedSymbols: z
|
||||
.string()
|
||||
.regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols")
|
||||
.optional()
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols)
|
||||
})
|
||||
.refine((data) => {
|
||||
return Object.values(data.required).some((count) => count > 0);
|
||||
}, "At least one character type must be required")
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length")
|
||||
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base);
|
||||
@@ -1,6 +1,17 @@
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
type TPasswordRequirements = {
|
||||
length: number;
|
||||
required: {
|
||||
lowercase: number;
|
||||
uppercase: number;
|
||||
digits: number;
|
||||
symbols: number;
|
||||
};
|
||||
allowedSymbols?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
@@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
export const generatePassword = () => {
|
||||
export const generatePassword = (passwordRequirements?: TPasswordRequirements) => {
|
||||
try {
|
||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
const chars = {
|
||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||
|
||||
@@ -33,6 +33,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("alias").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
@@ -45,7 +46,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
|
||||
const grouped = groupBy(rows, (r) => r.sshHostId);
|
||||
return Object.values(grouped).map((hostRows) => {
|
||||
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
|
||||
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } =
|
||||
hostRows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
||||
|
||||
@@ -59,6 +61,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
id: sshHostId,
|
||||
hostname,
|
||||
alias,
|
||||
projectId,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
@@ -87,6 +90,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("alias").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
@@ -99,7 +103,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
|
||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||
return Object.values(hostsGrouped).map((hostRows) => {
|
||||
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||
const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
hostRows.filter((r) => r.loginUser),
|
||||
@@ -116,6 +120,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
id: sshHostId,
|
||||
hostname,
|
||||
alias,
|
||||
projectId,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
@@ -144,6 +149,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
db.ref("hostname").withSchema(TableName.SshHost),
|
||||
db.ref("alias").withSchema(TableName.SshHost),
|
||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
@@ -155,7 +161,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||
const { sshHostId: id, projectId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||
|
||||
const loginMappingGrouped = groupBy(
|
||||
rows.filter((r) => r.loginUser),
|
||||
@@ -173,6 +179,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
projectId,
|
||||
hostname,
|
||||
alias,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
|
||||
@@ -6,6 +6,7 @@ export const sanitizedSshHost = SshHostsSchema.pick({
|
||||
id: true,
|
||||
projectId: true,
|
||||
hostname: true,
|
||||
alias: true,
|
||||
userCertTtl: true,
|
||||
hostCertTtl: true,
|
||||
userSshCaId: true,
|
||||
|
||||
@@ -119,6 +119,7 @@ export const sshHostServiceFactory = ({
|
||||
const createSshHost = async ({
|
||||
projectId,
|
||||
hostname,
|
||||
alias,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
@@ -192,6 +193,7 @@ export const sshHostServiceFactory = ({
|
||||
{
|
||||
projectId,
|
||||
hostname,
|
||||
alias: alias === "" ? null : alias,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
userSshCaId,
|
||||
@@ -265,6 +267,7 @@ export const sshHostServiceFactory = ({
|
||||
const updateSshHost = async ({
|
||||
sshHostId,
|
||||
hostname,
|
||||
alias,
|
||||
userCertTtl,
|
||||
hostCertTtl,
|
||||
loginMappings,
|
||||
@@ -297,6 +300,7 @@ export const sshHostServiceFactory = ({
|
||||
sshHostId,
|
||||
{
|
||||
hostname,
|
||||
alias: alias === "" ? null : alias,
|
||||
userCertTtl,
|
||||
hostCertTtl
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateSshHostDTO = {
|
||||
hostname: string;
|
||||
alias?: string;
|
||||
userCertTtl: string;
|
||||
hostCertTtl: string;
|
||||
loginMappings: {
|
||||
@@ -19,6 +20,7 @@ export type TCreateSshHostDTO = {
|
||||
export type TUpdateSshHostDTO = {
|
||||
sshHostId: string;
|
||||
hostname?: string;
|
||||
alias?: string;
|
||||
userCertTtl?: string;
|
||||
hostCertTtl?: string;
|
||||
loginMappings?: {
|
||||
|
||||
@@ -807,6 +807,8 @@ export const RAW_SECRETS = {
|
||||
tagIds: "The ID of the tags to be attached to the updated secret.",
|
||||
secretReminderRepeatDays: "Interval for secret rotation notifications, measured in days.",
|
||||
secretReminderNote: "Note to be attached in notification email.",
|
||||
secretReminderRecipients:
|
||||
"An array of user IDs that will receive the reminder email. If not specified, all project members will receive the reminder email.",
|
||||
newSecretName: "The new name for the secret."
|
||||
},
|
||||
DELETE: {
|
||||
@@ -1387,6 +1389,7 @@ export const SSH_HOSTS = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the SSH host in.",
|
||||
hostname: "The hostname of the SSH host.",
|
||||
alias: "The alias for the SSH host.",
|
||||
userCertTtl: "The time to live for user certificates issued under this host.",
|
||||
hostCertTtl: "The time to live for host certificates issued under this host.",
|
||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||
@@ -1401,6 +1404,7 @@ export const SSH_HOSTS = {
|
||||
UPDATE: {
|
||||
sshHostId: "The ID of the SSH host to update.",
|
||||
hostname: "The hostname of the SSH host to update to.",
|
||||
alias: "The alias for the SSH host to update to.",
|
||||
userCertTtl: "The time to live for user certificates issued under this host to update to.",
|
||||
hostCertTtl: "The time to live for host certificates issued under this host to update to.",
|
||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||
@@ -1857,6 +1861,20 @@ export const AppConnections = {
|
||||
WINDMILL: {
|
||||
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
|
||||
accessToken: "The access token to use to connect with Windmill."
|
||||
},
|
||||
LDAP: {
|
||||
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
|
||||
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
|
||||
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
|
||||
password: "The password to bind with for authentication.",
|
||||
sslRejectUnauthorized:
|
||||
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
|
||||
sslCertificate:
|
||||
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate."
|
||||
},
|
||||
TEAMCITY: {
|
||||
instanceUrl: "The TeamCity instance URL to connect with.",
|
||||
accessToken: "The access token to use to connect with TeamCity."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1996,6 +2014,10 @@ export const SecretSyncs = {
|
||||
WINDMILL: {
|
||||
workspace: "The Windmill workspace to sync secrets to.",
|
||||
path: "The Windmill workspace path to sync secrets to."
|
||||
},
|
||||
TEAMCITY: {
|
||||
project: "The TeamCity project to sync secrets to.",
|
||||
buildConfig: "The TeamCity build configuration to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2060,6 +2082,26 @@ export const SecretRotations = {
|
||||
},
|
||||
AUTH0_CLIENT_SECRET: {
|
||||
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
||||
},
|
||||
LDAP_PASSWORD: {
|
||||
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
|
||||
},
|
||||
GENERAL: {
|
||||
PASSWORD_REQUIREMENTS: {
|
||||
base: "The password requirements to use when generating the new password.",
|
||||
length: "The length of the password to generate.",
|
||||
required: {
|
||||
digits: "The amount of digits to require in the generated password.",
|
||||
lowercase: "The amount of lowercase characters to require in the generated password.",
|
||||
uppercase: "The amount of uppercase characters to require in the generated password.",
|
||||
symbols: "The amount of symbols to require in the generated password."
|
||||
},
|
||||
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").'
|
||||
}
|
||||
},
|
||||
AWS_IAM_USER_SECRET: {
|
||||
userName: "The name of the client to rotate credentials for.",
|
||||
region: "The AWS region the client is present in."
|
||||
}
|
||||
},
|
||||
SECRETS_MAPPING: {
|
||||
@@ -2070,6 +2112,14 @@ export const SecretRotations = {
|
||||
AUTH0_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."
|
||||
},
|
||||
LDAP_PASSWORD: {
|
||||
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
|
||||
password: "The name of the secret that the rotated password will be mapped to."
|
||||
},
|
||||
AWS_IAM_USER_SECRET: {
|
||||
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
|
||||
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const DistinguishedNameRegex =
|
||||
// DN format, ie; CN=user,OU=users,DC=example,DC=com
|
||||
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import fp from "fastify-plugin";
|
||||
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const injectAssumePrivilege = fp(async (server: FastifyZodProvider) => {
|
||||
server.addHook("onRequest", async (req, res) => {
|
||||
const assumeRoleCookie = req.cookies["infisical-project-assume-privileges"];
|
||||
try {
|
||||
if (req?.auth?.authMode === AuthMode.JWT && assumeRoleCookie) {
|
||||
const decodedToken = server.services.assumePrivileges.verifyAssumePrivilegeToken(
|
||||
assumeRoleCookie,
|
||||
req.auth.tokenVersionId
|
||||
);
|
||||
if (decodedToken) {
|
||||
requestContext.set("assumedPrivilegeDetails", decodedToken);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
req.log.error({ error }, "Failed to verify assume privilege token");
|
||||
void res.clearCookie("infisical-project-assume-privileges");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-appr
|
||||
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
|
||||
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
|
||||
import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
|
||||
import { assumePrivilegeServiceFactory } from "@app/ee/services/assume-privilege/assume-privilege-service";
|
||||
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
@@ -214,6 +215,7 @@ import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-f
|
||||
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
||||
import { secretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
||||
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||
import { secretReminderRecipientsDALFactory } from "@app/services/secret-reminder-recipients/secret-reminder-recipients-dal";
|
||||
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
|
||||
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
||||
import { secretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
|
||||
@@ -248,6 +250,7 @@ import { workflowIntegrationDALFactory } from "@app/services/workflow-integratio
|
||||
import { workflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
|
||||
|
||||
import { injectAuditLogInfo } from "../plugins/audit-log";
|
||||
import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege";
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||
@@ -417,6 +420,7 @@ export const registerRoutes = async (
|
||||
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
|
||||
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
|
||||
@@ -427,6 +431,11 @@ export const registerRoutes = async (
|
||||
serviceTokenDAL,
|
||||
projectDAL
|
||||
});
|
||||
const assumePrivilegeService = assumePrivilegeServiceFactory({
|
||||
projectDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const licenseService = licenseServiceFactory({
|
||||
permissionService,
|
||||
orgDAL,
|
||||
@@ -596,7 +605,14 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
|
||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, totpService });
|
||||
const loginService = authLoginServiceFactory({
|
||||
userDAL,
|
||||
smtpService,
|
||||
tokenService,
|
||||
orgDAL,
|
||||
totpService,
|
||||
auditLogService
|
||||
});
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
tokenService,
|
||||
smtpService,
|
||||
@@ -721,6 +737,7 @@ export const registerRoutes = async (
|
||||
projectKeyDAL,
|
||||
projectRoleDAL,
|
||||
groupProjectDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
});
|
||||
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
||||
@@ -954,6 +971,7 @@ export const registerRoutes = async (
|
||||
secretApprovalRequestDAL,
|
||||
projectKeyDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
orgService,
|
||||
resourceMetadataDAL,
|
||||
secretSyncQueue
|
||||
@@ -1015,7 +1033,9 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
identityDAL,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const snapshotService = secretSnapshotServiceFactory({
|
||||
@@ -1668,7 +1688,8 @@ export const registerRoutes = async (
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService,
|
||||
secretRotationV2: secretRotationV2Service
|
||||
secretRotationV2: secretRotationV2Service,
|
||||
assumePrivileges: assumePrivilegeService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@@ -1689,6 +1710,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||
await server.register(injectAssumePrivilege);
|
||||
await server.register(injectPermission);
|
||||
await server.register(injectRateLimits);
|
||||
await server.register(injectAuditLogInfo);
|
||||
|
||||
@@ -28,11 +28,16 @@ import {
|
||||
HumanitecConnectionListItemSchema,
|
||||
SanitizedHumanitecConnectionSchema
|
||||
} from "@app/services/app-connection/humanitec";
|
||||
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
} from "@app/services/app-connection/postgres";
|
||||
import {
|
||||
SanitizedTeamCityConnectionSchema,
|
||||
TeamCityConnectionListItemSchema
|
||||
} from "@app/services/app-connection/teamcity";
|
||||
import {
|
||||
SanitizedTerraformCloudConnectionSchema,
|
||||
TerraformCloudConnectionListItemSchema
|
||||
@@ -59,7 +64,9 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedMsSqlConnectionSchema.options,
|
||||
...SanitizedCamundaConnectionSchema.options,
|
||||
...SanitizedWindmillConnectionSchema.options,
|
||||
...SanitizedAuth0ConnectionSchema.options
|
||||
...SanitizedAuth0ConnectionSchema.options,
|
||||
...SanitizedLdapConnectionSchema.options,
|
||||
...SanitizedTeamCityConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -76,7 +83,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
MsSqlConnectionListItemSchema,
|
||||
CamundaConnectionListItemSchema,
|
||||
WindmillConnectionListItemSchema,
|
||||
Auth0ConnectionListItemSchema
|
||||
Auth0ConnectionListItemSchema,
|
||||
LdapConnectionListItemSchema,
|
||||
TeamCityConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -59,4 +59,40 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
return { kmsKeys };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/users`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
iamUsers: z
|
||||
.object({
|
||||
UserName: z.string(),
|
||||
Arn: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const iamUsers = await server.services.appConnection.aws.listIamUsers(
|
||||
{
|
||||
connectionId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return { iamUsers };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||
@@ -9,8 +9,10 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
||||
@@ -32,5 +34,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
||||
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateLdapConnectionSchema,
|
||||
SanitizedLdapConnectionSchema,
|
||||
UpdateLdapConnectionSchema
|
||||
} from "@app/services/app-connection/ldap";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerLdapConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.LDAP,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedLdapConnectionSchema,
|
||||
createSchema: CreateLdapConnectionSchema,
|
||||
updateSchema: UpdateLdapConnectionSchema
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateTeamCityConnectionSchema,
|
||||
SanitizedTeamCityConnectionSchema,
|
||||
UpdateTeamCityConnectionSchema
|
||||
} from "@app/services/app-connection/teamcity";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerTeamCityConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.TeamCity,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedTeamCityConnectionSchema,
|
||||
createSchema: CreateTeamCityConnectionSchema,
|
||||
updateSchema: UpdateTeamCityConnectionSchema
|
||||
});
|
||||
|
||||
// The following endpoints are for internal Infisical App use only and not part of the public API
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
buildTypes: z.object({
|
||||
buildType: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const projects = await server.services.appConnection.teamcity.listProjects(connectionId, req.permission);
|
||||
|
||||
return projects;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -33,6 +33,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
void res.cookie("infisical-project-assume-privileges", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED,
|
||||
maxAge: 0
|
||||
});
|
||||
|
||||
return { message: "Successfully logged out" };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
@@ -594,6 +594,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretReminderRecipients: z
|
||||
.object({
|
||||
user: UsersSchema.pick({ id: true, email: true, username: true }),
|
||||
id: z.string()
|
||||
})
|
||||
.array(),
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
ProjectSlackConfigsSchema,
|
||||
ProjectSshConfigsSchema,
|
||||
ProjectType,
|
||||
SecretFoldersSchema,
|
||||
SortDirection,
|
||||
@@ -78,7 +80,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
includeGroupMembers: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
.transform((value) => value === "true"),
|
||||
roles: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform(decodeURIComponent)
|
||||
.refine((value) => {
|
||||
if (!value) return true;
|
||||
const slugs = value.split(",");
|
||||
return slugs.every((slug) => slugify(slug.trim(), { lowercase: true }) === slug.trim());
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@@ -117,13 +129,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = (req.query.roles?.split(",") || []).filter(Boolean);
|
||||
const users = await server.services.projectMembership.getProjectMemberships({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
includeGroupMembers: req.query.includeGroupMembers,
|
||||
projectId: req.params.workspaceId,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorOrgId: req.permission.orgId,
|
||||
roles
|
||||
});
|
||||
|
||||
return { users };
|
||||
@@ -623,6 +637,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/ssh-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSshConfigsSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
projectId: true,
|
||||
defaultUserSshCaId: true,
|
||||
defaultHostSshCaId: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshConfig = await server.services.project.getProjectSshConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshConfig.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_SSH_CONFIG,
|
||||
metadata: {
|
||||
id: sshConfig.id,
|
||||
projectId: sshConfig.projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/ssh-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
defaultUserSshCaId: z.string().optional(),
|
||||
defaultHostSshCaId: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSshConfigsSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
projectId: true,
|
||||
defaultUserSshCaId: true,
|
||||
defaultHostSshCaId: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshConfig = await server.services.project.updateProjectSshConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: sshConfig.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_SSH_CONFIG,
|
||||
metadata: {
|
||||
id: sshConfig.id,
|
||||
projectId: sshConfig.projectId,
|
||||
defaultUserSshCaId: sshConfig.defaultUserSshCaId,
|
||||
defaultHostSshCaId: sshConfig.defaultHostSshCaId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return sshConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/slack-config",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
|
||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||
import { registerWindmillSyncRouter } from "./windmill-sync-router";
|
||||
@@ -27,5 +28,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||
[SecretSync.Windmill]: registerWindmillSyncRouter
|
||||
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
||||
[SecretSync.TeamCity]: registerTeamCitySyncRouter
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/service
|
||||
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
|
||||
@@ -39,7 +40,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
TerraformCloudSyncSchema,
|
||||
CamundaSyncSchema,
|
||||
VercelSyncSchema,
|
||||
WindmillSyncSchema
|
||||
WindmillSyncSchema,
|
||||
TeamCitySyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -54,7 +56,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
TerraformCloudSyncListItemSchema,
|
||||
CamundaSyncListItemSchema,
|
||||
VercelSyncListItemSchema,
|
||||
WindmillSyncListItemSchema
|
||||
WindmillSyncListItemSchema,
|
||||
TeamCitySyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
CreateTeamCitySyncSchema,
|
||||
TeamCitySyncSchema,
|
||||
UpdateTeamCitySyncSchema
|
||||
} from "@app/services/secret-sync/teamcity";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerTeamCitySyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.TeamCity,
|
||||
server,
|
||||
responseSchema: TeamCitySyncSchema,
|
||||
createSchema: CreateTeamCitySyncSchema,
|
||||
updateSchema: UpdateTeamCitySyncSchema
|
||||
});
|
||||
@@ -252,6 +252,31 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/me/sessions/:sessionId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sessionId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.authToken.revokeMySessionById(req.permission.id, req.params.sessionId);
|
||||
return {
|
||||
message: "Successfully revoked session"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me",
|
||||
|
||||
@@ -77,6 +77,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
secure: cfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
void res.cookie("infisical-project-assume-privileges", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: cfg.HTTPS_ENABLED,
|
||||
maxAge: 0
|
||||
});
|
||||
|
||||
return { token: tokens.access, isMfaEnabled: false };
|
||||
}
|
||||
});
|
||||
@@ -131,6 +139,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
void res.cookie("infisical-project-assume-privileges", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED,
|
||||
maxAge: 0
|
||||
});
|
||||
|
||||
return {
|
||||
encryptionVersion: data.user.encryptionVersion,
|
||||
token: data.token.access,
|
||||
|
||||
@@ -662,6 +662,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays),
|
||||
secretReminderRecipients: z.string().array().optional().describe(RAW_SECRETS.UPDATE.secretReminderRecipients),
|
||||
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
||||
secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment)
|
||||
}),
|
||||
@@ -692,6 +693,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
skipMultilineEncoding: req.body.skipMultilineEncoding,
|
||||
tagIds: req.body.tagIds,
|
||||
secretReminderRepeatDays: req.body.secretReminderRepeatDays,
|
||||
secretReminderRecipients: req.body.secretReminderRecipients,
|
||||
secretReminderNote: req.body.secretReminderNote,
|
||||
metadata: req.body.metadata,
|
||||
newSecretName: req.body.newSecretName,
|
||||
|
||||
@@ -12,7 +12,9 @@ export enum AppConnection {
|
||||
MsSql = "mssql",
|
||||
Camunda = "camunda",
|
||||
Windmill = "windmill",
|
||||
Auth0 = "auth0"
|
||||
Auth0 = "auth0",
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -41,8 +41,14 @@ import {
|
||||
HumanitecConnectionMethod,
|
||||
validateHumanitecConnectionCredentials
|
||||
} from "./humanitec";
|
||||
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import {
|
||||
getTeamCityConnectionListItem,
|
||||
TeamCityConnectionMethod,
|
||||
validateTeamCityConnectionCredentials
|
||||
} from "./teamcity";
|
||||
import {
|
||||
getTerraformCloudConnectionListItem,
|
||||
TerraformCloudConnectionMethod,
|
||||
@@ -71,7 +77,9 @@ export const listAppConnectionOptions = () => {
|
||||
getMsSqlConnectionListItem(),
|
||||
getCamundaConnectionListItem(),
|
||||
getWindmillConnectionListItem(),
|
||||
getAuth0ConnectionListItem()
|
||||
getAuth0ConnectionListItem(),
|
||||
getLdapConnectionListItem(),
|
||||
getTeamCityConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -135,7 +143,9 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -167,9 +177,12 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
case Auth0ConnectionMethod.ClientCredentials:
|
||||
return "Client Credentials";
|
||||
case LdapConnectionMethod.SimpleBind:
|
||||
return "Simple Bind";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@@ -214,5 +227,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -14,5 +14,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||
[AppConnection.Camunda]: "Camunda",
|
||||
[AppConnection.Windmill]: "Windmill",
|
||||
[AppConnection.Auth0]: "Auth0"
|
||||
[AppConnection.Auth0]: "Auth0",
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity"
|
||||
};
|
||||
|
||||
@@ -43,8 +43,11 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
|
||||
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
|
||||
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
|
||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
@@ -74,7 +77,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -450,6 +455,7 @@ export const appConnectionServiceFactory = ({
|
||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
vercel: vercelConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -57,12 +57,24 @@ import {
|
||||
THumanitecConnectionInput,
|
||||
TValidateHumanitecConnectionCredentialsSchema
|
||||
} from "./humanitec";
|
||||
import {
|
||||
TLdapConnection,
|
||||
TLdapConnectionConfig,
|
||||
TLdapConnectionInput,
|
||||
TValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import {
|
||||
TPostgresConnection,
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
TTeamCityConnectionInput,
|
||||
TValidateTeamCityConnectionCredentialsSchema
|
||||
} from "./teamcity";
|
||||
import {
|
||||
TTerraformCloudConnection,
|
||||
TTerraformCloudConnectionConfig,
|
||||
@@ -97,6 +109,8 @@ export type TAppConnection = { id: string } & (
|
||||
| TCamundaConnection
|
||||
| TWindmillConnection
|
||||
| TAuth0Connection
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -118,6 +132,8 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TCamundaConnectionInput
|
||||
| TWindmillConnectionInput
|
||||
| TAuth0ConnectionInput
|
||||
| TLdapConnectionInput
|
||||
| TTeamCityConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
@@ -144,7 +160,9 @@ export type TAppConnectionConfig =
|
||||
| TSqlConnectionConfig
|
||||
| TCamundaConnectionConfig
|
||||
| TWindmillConnectionConfig
|
||||
| TAuth0ConnectionConfig;
|
||||
| TAuth0ConnectionConfig
|
||||
| TLdapConnectionConfig
|
||||
| TTeamCityConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -160,7 +178,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema
|
||||
| TValidateWindmillConnectionCredentialsSchema
|
||||
| TValidateAuth0ConnectionCredentialsSchema;
|
||||
| TValidateAuth0ConnectionCredentialsSchema
|
||||
| TValidateLdapConnectionCredentialsSchema
|
||||
| TValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
@@ -168,6 +188,10 @@ export type TListAwsConnectionKmsKeys = {
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
||||
export type TListAwsConnectionIamUsers = {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionCredentialsValidator = (
|
||||
appConnection: TAppConnectionConfig
|
||||
) => Promise<TAppConnection["credentials"]>;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import AWS from "aws-sdk";
|
||||
import { AxiosError } from "axios";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||
@@ -90,9 +92,20 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
||||
const sts = new AWS.STS(awsConfig);
|
||||
|
||||
resp = await sts.getCallerIdentity().promise();
|
||||
} catch (e: unknown) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error validating AWS connection credentials");
|
||||
|
||||
let message: string;
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message = (error.response?.data?.message as string) || error.message || "verify credentials";
|
||||
} else {
|
||||
message = (error as Error)?.message || "verify credentials";
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
message: `Unable to validate connection: ${message}`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import AWS from "aws-sdk";
|
||||
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
||||
import {
|
||||
TListAwsConnectionIamUsers,
|
||||
TListAwsConnectionKmsKeys
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -70,6 +73,23 @@ const listAwsKmsKeys = async (
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
const listAwsIamUsers = async (appConnection: TAwsConnection) => {
|
||||
const { credentials } = await getAwsConnectionConfig(appConnection);
|
||||
|
||||
const iam = new AWS.IAM({ credentials });
|
||||
|
||||
const userEntries: AWS.IAM.User[] = [];
|
||||
let userMarker: string | undefined;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await iam.listUsers({ MaxItems: 100, Marker: userMarker }).promise();
|
||||
userEntries.push(...(response.Users || []));
|
||||
userMarker = response.Marker;
|
||||
} while (userMarker);
|
||||
|
||||
return userEntries;
|
||||
};
|
||||
|
||||
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listKmsKeys = async (
|
||||
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||
@@ -82,7 +102,16 @@ export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
const listIamUsers = async ({ connectionId }: TListAwsConnectionIamUsers, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||
|
||||
const iamUsers = await listAwsIamUsers(appConnection);
|
||||
|
||||
return iamUsers;
|
||||
};
|
||||
|
||||
return {
|
||||
listKmsKeys
|
||||
listKmsKeys,
|
||||
listIamUsers
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./ldap-connection-enums";
|
||||
export * from "./ldap-connection-fns";
|
||||
export * from "./ldap-connection-schemas";
|
||||
export * from "./ldap-connection-types";
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum LdapConnectionMethod {
|
||||
SimpleBind = "simple-bind"
|
||||
}
|
||||
|
||||
export enum LdapProvider {
|
||||
ActiveDirectory = "active-directory"
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import ldap from "ldapjs";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { LdapConnectionMethod } from "./ldap-connection-enums";
|
||||
import { TLdapConnectionConfig } from "./ldap-connection-types";
|
||||
|
||||
export const getLdapConnectionListItem = () => {
|
||||
return {
|
||||
name: "LDAP" as const,
|
||||
app: AppConnection.LDAP as const,
|
||||
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
|
||||
};
|
||||
};
|
||||
|
||||
const LDAP_TIMEOUT = 15_000;
|
||||
|
||||
export const getLdapConnectionClient = async ({
|
||||
url,
|
||||
dn,
|
||||
password,
|
||||
sslCertificate,
|
||||
sslRejectUnauthorized = true
|
||||
}: TLdapConnectionConfig["credentials"]) => {
|
||||
await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const isSSL = url.startsWith("ldaps");
|
||||
|
||||
return new Promise<ldap.Client>((resolve, reject) => {
|
||||
const client = ldap.createClient({
|
||||
url,
|
||||
timeout: LDAP_TIMEOUT,
|
||||
connectTimeout: LDAP_TIMEOUT,
|
||||
tlsOptions: isSSL
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate ? [sslCertificate] : undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
client.on("error", (err: Error) => {
|
||||
logger.error(err, "LDAP Error");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Error - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectError", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Error");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connect Error - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectRefused", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Refused");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connection Refused - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connectTimeout", (err: Error) => {
|
||||
logger.error(err, "LDAP Connection Timeout");
|
||||
client.destroy();
|
||||
reject(new Error(`Provider Connection Timeout - ${err.message}`));
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
client.bind(dn, password, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Bind Error");
|
||||
reject(new Error(`Bind Error: ${err.message}`));
|
||||
client.destroy();
|
||||
}
|
||||
|
||||
resolve(client);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
|
||||
let client: ldap.Client | undefined;
|
||||
|
||||
try {
|
||||
client = await getLdapConnectionClient(credentials);
|
||||
|
||||
// this shouldn't occur as handle connection error events in client but here as fallback
|
||||
if (!client.connected) {
|
||||
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
|
||||
}
|
||||
|
||||
return credentials;
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
|
||||
});
|
||||
} finally {
|
||||
client?.destroy();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
|
||||
|
||||
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
|
||||
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "URL required")
|
||||
.regex(new RE2(/^ldaps?:\/\//))
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.url),
|
||||
dn: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||
.min(1, "Distinguished Name (DN) required")
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.dn),
|
||||
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
|
||||
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
|
||||
});
|
||||
|
||||
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.LDAP)
|
||||
});
|
||||
|
||||
export const LdapConnectionSchema = z.intersection(
|
||||
BaseLdapConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseLdapConnectionSchema.extend({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
|
||||
provider: true,
|
||||
url: true,
|
||||
dn: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.LDAP).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
|
||||
);
|
||||
|
||||
export const UpdateLdapConnectionSchema = z
|
||||
.object({
|
||||
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.LDAP).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
|
||||
|
||||
export const LdapConnectionListItemSchema = z.object({
|
||||
name: z.literal("LDAP"),
|
||||
app: z.literal(AppConnection.LDAP),
|
||||
// the below is preferable but currently breaks with our zod to json schema parser
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(LdapConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
CreateLdapConnectionSchema,
|
||||
LdapConnectionSchema,
|
||||
ValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap-connection-schemas";
|
||||
|
||||
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
|
||||
|
||||
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
|
||||
app: AppConnection.LDAP;
|
||||
};
|
||||
|
||||
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
|
||||
|
||||
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
@@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./teamcity-connection-enums";
|
||||
export * from "./teamcity-connection-fns";
|
||||
export * from "./teamcity-connection-schemas";
|
||||
export * from "./teamcity-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum TeamCityConnectionMethod {
|
||||
AccessToken = "access-token"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||
import {
|
||||
TTeamCityConnection,
|
||||
TTeamCityConnectionConfig,
|
||||
TTeamCityListProjectsResponse
|
||||
} from "./teamcity-connection-types";
|
||||
|
||||
export const getTeamCityInstanceUrl = async (config: TTeamCityConnectionConfig) => {
|
||||
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||
|
||||
return instanceUrl;
|
||||
};
|
||||
|
||||
export const getTeamCityConnectionListItem = () => {
|
||||
return {
|
||||
name: "TeamCity" as const,
|
||||
app: AppConnection.TeamCity as const,
|
||||
methods: Object.values(TeamCityConnectionMethod) as [TeamCityConnectionMethod.AccessToken]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateTeamCityConnectionCredentials = async (config: TTeamCityConnectionConfig) => {
|
||||
const instanceUrl = await getTeamCityInstanceUrl(config);
|
||||
|
||||
const { accessToken } = config.credentials;
|
||||
|
||||
try {
|
||||
await request.get(`${instanceUrl}/app/rest/server`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
|
||||
export const listTeamCityProjects = async (appConnection: TTeamCityConnection) => {
|
||||
const instanceUrl = await getTeamCityInstanceUrl(appConnection);
|
||||
const { accessToken } = appConnection.credentials;
|
||||
|
||||
const resp = await request.get<TTeamCityListProjectsResponse>(
|
||||
`${instanceUrl}/app/rest/projects?fields=project(id,name,buildTypes(buildType(id,name)))`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Filter out the root project. Should not be seen by users.
|
||||
return resp.data.project.filter((proj) => proj.id !== "_Root");
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { TeamCityConnectionMethod } from "./teamcity-connection-enums";
|
||||
|
||||
export const TeamCityConnectionAccessTokenCredentialsSchema = z.object({
|
||||
accessToken: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Access Token required")
|
||||
.describe(AppConnections.CREDENTIALS.TEAMCITY.accessToken),
|
||||
instanceUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Invalid Instance URL")
|
||||
.min(1, "Instance URL required")
|
||||
.describe(AppConnections.CREDENTIALS.TEAMCITY.instanceUrl)
|
||||
});
|
||||
|
||||
const BaseTeamCityConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.TeamCity) });
|
||||
|
||||
export const TeamCityConnectionSchema = BaseTeamCityConnectionSchema.extend({
|
||||
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedTeamCityConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseTeamCityConnectionSchema.extend({
|
||||
method: z.literal(TeamCityConnectionMethod.AccessToken),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.pick({
|
||||
instanceUrl: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateTeamCityConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(TeamCityConnectionMethod.AccessToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.TeamCity).method),
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.TeamCity).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateTeamCityConnectionSchema = ValidateTeamCityConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.TeamCity)
|
||||
);
|
||||
|
||||
export const UpdateTeamCityConnectionSchema = z
|
||||
.object({
|
||||
credentials: TeamCityConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.TeamCity).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.TeamCity));
|
||||
|
||||
export const TeamCityConnectionListItemSchema = z.object({
|
||||
name: z.literal("TeamCity"),
|
||||
app: z.literal(AppConnection.TeamCity),
|
||||
methods: z.nativeEnum(TeamCityConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listTeamCityProjects } from "./teamcity-connection-fns";
|
||||
import { TTeamCityConnection } from "./teamcity-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TTeamCityConnection>;
|
||||
|
||||
export const teamcityConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.TeamCity, connectionId, actor);
|
||||
|
||||
try {
|
||||
const projects = await listTeamCityProjects(appConnection);
|
||||
return projects;
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listProjects
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateTeamCityConnectionSchema,
|
||||
TeamCityConnectionSchema,
|
||||
ValidateTeamCityConnectionCredentialsSchema
|
||||
} from "./teamcity-connection-schemas";
|
||||
|
||||
export type TTeamCityConnection = z.infer<typeof TeamCityConnectionSchema>;
|
||||
|
||||
export type TTeamCityConnectionInput = z.infer<typeof CreateTeamCityConnectionSchema> & {
|
||||
app: AppConnection.TeamCity;
|
||||
};
|
||||
|
||||
export type TValidateTeamCityConnectionCredentialsSchema = typeof ValidateTeamCityConnectionCredentialsSchema;
|
||||
|
||||
export type TTeamCityConnectionConfig = DiscriminativePick<
|
||||
TTeamCityConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TTeamCityProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TTeamCityProjectWithBuildTypes = TTeamCityProject & {
|
||||
buildTypes: {
|
||||
buildType: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TTeamCityListProjectsResponse = {
|
||||
project: TTeamCityProjectWithBuildTypes[];
|
||||
};
|
||||
@@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||
try {
|
||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter);
|
||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession)
|
||||
.where(filter)
|
||||
.orderBy("lastUsed", "desc");
|
||||
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Find all token session", error });
|
||||
|
||||
@@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
|
||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||
|
||||
const revokeMySessionById = async (userId: string, sessionId: string) =>
|
||||
tokenDAL.deleteTokenSession({ userId, id: sessionId });
|
||||
|
||||
const validateRefreshToken = async (refreshToken?: string) => {
|
||||
const appCfg = getConfig();
|
||||
if (!refreshToken)
|
||||
@@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
clearTokenSessionById,
|
||||
getTokenSessionByUser,
|
||||
revokeAllMySessions,
|
||||
revokeMySessionById,
|
||||
validateRefreshToken,
|
||||
fnValidateJwtIdentity,
|
||||
getUserTokenSessionById
|
||||
|
||||
@@ -3,6 +3,8 @@ import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
@@ -10,7 +12,9 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
@@ -28,7 +32,14 @@ import {
|
||||
TOauthTokenExchangeDTO,
|
||||
TVerifyMfaTokenDTO
|
||||
} from "./auth-login-type";
|
||||
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "./auth-type";
|
||||
import {
|
||||
ActorType,
|
||||
AuthMethod,
|
||||
AuthModeJwtTokenPayload,
|
||||
AuthModeMfaJwtTokenPayload,
|
||||
AuthTokenType,
|
||||
MfaMethod
|
||||
} from "./auth-type";
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
@@ -36,6 +47,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -44,7 +56,8 @@ export const authLoginServiceFactory = ({
|
||||
tokenService,
|
||||
smtpService,
|
||||
orgDAL,
|
||||
totpService
|
||||
totpService,
|
||||
auditLogService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@@ -412,6 +425,55 @@ export const authLoginServiceFactory = ({
|
||||
mfaMethod: decodedToken.mfaMethod
|
||||
});
|
||||
|
||||
// In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced)
|
||||
if (
|
||||
selectedOrg.authEnforced &&
|
||||
selectedOrg.bypassOrgAuthEnabled &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC
|
||||
) {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
userAgentType: getUserAgentType(userAgent),
|
||||
actor: {
|
||||
type: ActorType.USER,
|
||||
metadata: {
|
||||
email: user.email,
|
||||
userId: user.id,
|
||||
username: user.username
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.ORG_ADMIN_BYPASS_SSO,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Notify all admins via email (besides the actor)
|
||||
const orgAdmins = await orgDAL.findOrgMembersByRole(organizationId, OrgMembershipRole.Admin);
|
||||
const adminEmails = orgAdmins
|
||||
.filter((admin) => admin.user.id !== user.id)
|
||||
.map((admin) => admin.user.email)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (adminEmails.length > 0) {
|
||||
await smtpService.sendMail({
|
||||
recipients: adminEmails,
|
||||
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||
substitutions: {
|
||||
email: user.email,
|
||||
timestamp: new Date().toISOString(),
|
||||
ip: ipAddress,
|
||||
userAgent,
|
||||
siteUrl: removeTrailingSlash(cfg.SITE_URL || "https://app.infisical.com")
|
||||
},
|
||||
template: SmtpTemplates.OrgAdminBreakglassAccess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...tokens,
|
||||
isMfaEnabled: false
|
||||
|
||||
@@ -342,9 +342,12 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
return async ({ cipherTextBlob }: Pick<TDecryptWithKmsDTO, "cipherTextBlob">) => {
|
||||
const { data } = await externalKms.decrypt(cipherTextBlob);
|
||||
|
||||
return data;
|
||||
try {
|
||||
const { data } = await externalKms.decrypt(cipherTextBlob);
|
||||
return data;
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -557,9 +560,12 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
return async ({ plainText }: Pick<TEncryptWithKmsDTO, "plainText">) => {
|
||||
const { encryptedBlob } = await externalKms.encrypt(plainText);
|
||||
|
||||
return { cipherTextBlob: encryptedBlob };
|
||||
try {
|
||||
const { encryptedBlob } = await externalKms.encrypt(plainText);
|
||||
return { cipherTextBlob: encryptedBlob };
|
||||
} finally {
|
||||
await externalKms.cleanup();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -787,13 +793,19 @@ export const kmsServiceFactory = ({
|
||||
return projectDataKey;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`getProjectSecretManagerKmsDataKey: Failed to get project data key for [projectId=${projectId}]`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await lock?.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.kmsSecretManagerEncryptedDataKey) {
|
||||
throw new Error("Missing project data key");
|
||||
throw new BadRequestError({ message: "Missing project data key" });
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
TableName,
|
||||
TOrganizations,
|
||||
TOrganizationsInsert,
|
||||
@@ -216,9 +217,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
|
||||
try {
|
||||
const conn = tx || db;
|
||||
const conn = tx || db.replicaNode();
|
||||
const members = await conn(TableName.OrgMembership)
|
||||
// .replicaNode()(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
@@ -251,6 +251,43 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgMembersByRole = async (orgId: string, role: OrgMembershipRole, tx?: Knex) => {
|
||||
try {
|
||||
const conn = tx || db.replicaNode();
|
||||
const members = await conn(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.where(`${TableName.OrgMembership}.role`, role)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
conn.ref("id").withSchema(TableName.OrgMembership),
|
||||
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
conn.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("role").withSchema(TableName.OrgMembership),
|
||||
conn.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("status").withSchema(TableName.OrgMembership),
|
||||
conn.ref("username").withSchema(TableName.Users),
|
||||
conn.ref("email").withSchema(TableName.Users),
|
||||
conn.ref("firstName").withSchema(TableName.Users),
|
||||
conn.ref("lastName").withSchema(TableName.Users),
|
||||
conn.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
return members.map(({ username, email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||
...data,
|
||||
user: { username, email, firstName, lastName, id: userId, publicKey }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org members by role" });
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgGhostUser = async (orgId: string) => {
|
||||
try {
|
||||
const member = await db
|
||||
@@ -472,6 +509,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
findAllOrgsByUserId,
|
||||
ghostUserExists,
|
||||
findOrgMembersByUsername,
|
||||
findOrgMembersByRole,
|
||||
findOrgGhostUser,
|
||||
create,
|
||||
updateById,
|
||||
|
||||
@@ -13,7 +13,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
// special query
|
||||
const findAllProjectMembers = async (
|
||||
projectId: string,
|
||||
filter: { usernames?: string[]; username?: string; id?: string } = {}
|
||||
filter: { usernames?: string[]; username?: string; id?: string; roles?: string[] } = {}
|
||||
) => {
|
||||
try {
|
||||
const docs = await db
|
||||
@@ -31,6 +31,29 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
if (filter.id) {
|
||||
void qb.where(`${TableName.ProjectMembership}.id`, filter.id);
|
||||
}
|
||||
if (filter.roles && filter.roles.length > 0) {
|
||||
void qb.whereExists((subQuery) => {
|
||||
void subQuery
|
||||
.select("role")
|
||||
.from(TableName.ProjectUserMembershipRole)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.ProjectRoles}.id`,
|
||||
`${TableName.ProjectUserMembershipRole}.customRoleId`
|
||||
)
|
||||
.whereRaw("??.?? = ??.??", [
|
||||
TableName.ProjectUserMembershipRole,
|
||||
"projectMembershipId",
|
||||
TableName.ProjectMembership,
|
||||
"id"
|
||||
])
|
||||
.where((subQb) => {
|
||||
void subQb
|
||||
.whereIn(`${TableName.ProjectUserMembershipRole}.role`, filter.roles as string[])
|
||||
.orWhereIn(`${TableName.ProjectRoles}.slug`, filter.roles as string[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.join<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { TSecretReminderRecipientsDALFactory } from "../secret-reminder-recipients/secret-reminder-recipients-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
@@ -53,6 +54,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||
groupProjectDAL: TGroupProjectDALFactory;
|
||||
};
|
||||
|
||||
@@ -71,6 +73,7 @@ export const projectMembershipServiceFactory = ({
|
||||
groupProjectDAL,
|
||||
projectDAL,
|
||||
projectKeyDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
}: TProjectMembershipServiceFactoryDep) => {
|
||||
const getProjectMemberships = async ({
|
||||
@@ -79,7 +82,8 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
includeGroupMembers,
|
||||
projectId
|
||||
projectId,
|
||||
roles
|
||||
}: TGetProjectMembershipDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -91,7 +95,7 @@ export const projectMembershipServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
||||
|
||||
// projectMembers[0].project
|
||||
if (includeGroupMembers) {
|
||||
@@ -388,6 +392,13 @@ export const projectMembershipServiceFactory = ({
|
||||
const membership = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx);
|
||||
await projectKeyDAL.delete({ receiverId: deletedMembership.userId, projectId }, tx);
|
||||
await secretReminderRecipientsDAL.delete(
|
||||
{
|
||||
projectId,
|
||||
userId: deletedMembership.userId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return deletedMembership;
|
||||
});
|
||||
return membership;
|
||||
@@ -465,6 +476,16 @@ export const projectMembershipServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
await secretReminderRecipientsDAL.delete(
|
||||
{
|
||||
projectId,
|
||||
$in: {
|
||||
userId: projectMembers.map(({ user }) => user.id)
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
// delete project keys belonging to users that are not part of any other groups in the project
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
@@ -525,6 +546,15 @@ export const projectMembershipServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await secretReminderRecipientsDAL.delete(
|
||||
{
|
||||
projectId,
|
||||
userId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const membership = (
|
||||
await projectMembershipDAL.delete(
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean } & TProjectPermission;
|
||||
export type TGetProjectMembershipDTO = { includeGroupMembers?: boolean; roles?: string[] } & TProjectPermission;
|
||||
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
|
||||
export enum ProjectUserMembershipTemporaryMode {
|
||||
Relative = "relative"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user